diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..3b560c53e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Global code owner +* @paocela diff --git a/.github/instructions/backend.instructions.md b/.github/instructions/backend.instructions.md new file mode 100644 index 000000000..663856481 --- /dev/null +++ b/.github/instructions/backend.instructions.md @@ -0,0 +1,315 @@ +--- +applyTo: '/server/**' +--- + +# Backend Development Instructions + +## Architecture Overview + +**Full-Stack Structure**: Express.js backend + Angular frontend, deployed together. The server serves both API endpoints (`/api/*`) and static Angular files in production. + +**Migration Context**: This backend was migrated from Genezio infrastructure to self-hosted Express.js. The `server/src/*_interface.ts` files are legacy Genezio files; new code uses the Service/Controller/Route pattern. + +### Core Architecture Pattern: Service → Controller → Route + +``` +Request → Route → Middleware → Controller → Service → Database +``` + +**Example Flow**: +```typescript +// 1. Route (auth.routes.ts) +router.post('/login', authController.login); + +// 2. Controller (auth.controller.ts) - handles req/res +async login(req: Request, res: Response) { + const result = await authService.login(req.body); + res.json(result); +} + +// 3. Service (auth.service.ts) - business logic +async login(loginData: LoginRequest): Promise { + const user = await this.pool.query('SELECT ...'); + return { success: true, token: jwt.sign(...) }; +} +``` + +## Directory Structure + +``` +server/src/ +├── server.ts # Express app entry point +├── config/ +│ ├── db.ts # PostgreSQL pool singleton +│ └── logger.ts # Winston logging configuration +├── middleware/ +│ └── auth.middleware.ts # JWT validation (authMiddleware, adminMiddleware) +├── routes/ # Express routers, mounted at /api/* +├── controllers/ # Request/response handlers +├── services/ # Business logic, database queries +├── *_interface.ts # Legacy Genezio files (DO NOT MODIFY) +└── email_templates.ts # Email HTML templates +``` + +## Database Access Pattern + +**Singleton Pool Injection**: All services receive the database pool via constructor. + +```typescript +// config/db.ts exports a singleton +const pool = new Pool({ connectionString: process.env.RACEFORFEDERICA_DB_DATABASE_URL }); + +// Services accept pool via constructor +export class AuthService { + constructor(private pool: pg.Pool) {} + + async getUsers(): Promise { + const result = await this.pool.query('SELECT * FROM users'); + return result.rows; + } +} + +// Controllers instantiate services with shared pool +import pool from '../config/db.js'; +const authService = new AuthService(pool); +``` + +**Database Schema**: Entry-based result tables (`race_result_entries`, `sprint_result_entries`, etc.) store positions as rows, not columns. See `.github/instructions/db.instructions.md` for schema details. + +## Authentication & Authorization + +### Two-Level Middleware Chain + +**authMiddleware** (JWT validation): +- Extracts `Authorization: Bearer ` header +- Verifies JWT signature with `process.env.JWT_SECRET` +- Decodes token payload: `{ userId, username, isAdmin }` +- Attaches `req.user` for downstream use +- Returns 401 if token missing/invalid/expired + +**adminMiddleware** (admin check): +- Requires `authMiddleware` to run first +- Checks `req.user.isAdmin === true` +- Returns 403 if user not admin + +### Route Protection Patterns + +```typescript +// Public - no auth +router.post('/login', authController.login); + +// Protected - requires valid JWT +router.post('/profile', authMiddleware, authController.getProfile); + +// Admin only - requires JWT + admin flag +router.post('/users', authMiddleware, adminMiddleware, authController.getUsers); +``` + +**CRITICAL**: Always use both middleware for admin routes: `authMiddleware, adminMiddleware` (in that order). + +### JWT Token Structure + +```typescript +// Token created in auth.service.ts generateJWTToken() +jwt.sign({ + userId: user.id, + username: user.username, + isAdmin: user.is_admin // From database +}, process.env.JWT_SECRET, { expiresIn: '24h' }); +``` + +Session management: Tokens stored in `user_sessions` table with expiration tracking. + +## Logging with Winston + +**Logger Location**: `server/src/config/logger.ts` + +### Log Levels (priority order) +1. **error**: Critical errors (database failures, authentication errors) +2. **warn**: Potentially harmful situations (rate limits, deprecated usage) +3. **info**: Application state (server start, successful operations) +4. **http**: HTTP requests (handled by Morgan middleware) +5. **debug**: Detailed debugging info + +### Usage Pattern + +```typescript +import logger from '../config/logger.js'; + +// Structured logging with metadata +logger.error('Database query failed', { + error: err.message, + stack: err.stack, + userId: req.user?.userId +}); + +logger.info('User logged in', { userId: user.id, username: user.username }); + +// HTTP logging - automatic via Morgan in server.ts +// No manual HTTP logging needed +``` + +**Environment Behavior**: +- **Development**: Colorized console logs, level=debug +- **Production**: JSON files in `server/logs/` (error.log, combined.log), level=info + +**Configuration**: Set `LOG_LEVEL` in `.env` (error|warn|info|http|debug) + +## Error Handling Pattern + +**Controllers**: Always wrap service calls in try-catch + +```typescript +async someMethod(req: Request, res: Response): Promise { + try { + const result = await service.doSomething(req.body); + res.json(result); + } catch (error) { + logger.error('Operation failed', { error: error.message }); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Operation failed' + }); + } +} +``` + +**Services**: Return structured responses with `success` boolean + +```typescript +return { + success: false, + message: 'User not found' +}; +``` + +## Development Commands + +```bash +# Development with hot reload +npm run dev # Uses tsx watch + +# Build TypeScript +npm run build # Outputs to dist/ + +# Production +npm start # Runs compiled dist/server.js + +# Environment setup +cp .env.example .env # Then edit with actual credentials +``` + +**Required Environment Variables** (`.env`): +```env +RACEFORFEDERICA_DB_DATABASE_URL=postgresql://... +JWT_SECRET=your-secret-key +MAIL_USER=email@example.com +MAIL_PASS=email-password +RACEFORFEDERICA_DREANDOS_SECRET=twitch-secret +PORT=3000 +NODE_ENV=development +LOG_LEVEL=debug +``` + +## API Endpoint Conventions + +**Base Path**: All API routes mounted under `/api/*` + +**HTTP Method**: Use correct HTTP methods (GET, POST, PUT, DELETE) according to REST conventions. + +**Request Body**: Optional parameters handled with optional chaining +```typescript +const seasonId = req.body?.seasonId ? parseInt(req.body.seasonId) : undefined; +``` + +**Response Format**: Always include `success` boolean +```typescript +{ success: true, data: [...] } +{ success: false, message: 'Error description' } +``` +## Unit Testing +When implementing a new feature or fixing/refactoring, create unit tests for all possible scenarios, including edge cases. +tests must mock database interactions and external resources to ensure isolation and reliability. +**Testing Framework**: vitest +**Test Files**: server/tests/*.test.ts + + +## Testing with Postman + +Pre-configured collections in `server/`: +- `F123Dashboard.postman_collection.json` - All API endpoints +- `F123Dashboard.postman_environment.json` - Local env +- `F123Dashboard.postman_environment.prod.json` - Production env + +Auto-authentication flow implemented (see `server/POSTMAN_README.md`) + +## Common Patterns + +### Adding a New API Endpoint + +1. **Service** (`services/*.service.ts`): +```typescript +async getNewData(param: string): Promise { + const result = await this.pool.query('SELECT * FROM table WHERE id = $1', [param]); + return result.rows; +} +``` + +2. **Controller** (`controllers/*.controller.ts`): +```typescript +async getNewData(req: Request, res: Response): Promise { + try { + const data = await service.getNewData(req.body.param); + res.json(data); + } catch (error) { + logger.error('Error getting data:', error); + res.status(500).json({ success: false, message: 'Failed to get data' }); + } +} +``` + +3. **Route** (`routes/*.routes.ts`): +```typescript +router.post('/new-data', (req, res) => controller.getNewData(req, res)); +// Add authMiddleware if protected +``` + +4. **Import in server.ts**: Route already mounted if using existing router + +### Database Query Pattern + +Use parameterized queries to prevent SQL injection: +```typescript +// ✅ Correct +await pool.query('SELECT * FROM users WHERE id = $1', [userId]); + +// ❌ Wrong - SQL injection risk +await pool.query(`SELECT * FROM users WHERE id = ${userId}`); +``` + +## ES Modules Configuration + +**Important**: This project uses ES modules (`"type": "module"` in package.json) + +- Use `.js` extensions in imports: `import x from './file.js'` +- Use `import` syntax, not `require()` +- Get `__dirname` via: +```typescript +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +``` + +## Key Files Reference + +- **Architecture**: `server/src/server.ts` - Express app setup, middleware chain +- **Auth Flow**: `server/docs/authentication-flow.md` - Complete auth documentation +- **Database Schema**: `.github/instructions/db.instructions.md` - Entry-based result tables +- **Logging Guide**: `server/docs/winston-logging.md` - Winston usage and best practices + +## Production Deployment + +**Static Files**: Express serves Angular build from `client/dist/browser/` in production +**Fallback**: All non-API routes return `index.html` for Angular routing +**Environment**: Set `NODE_ENV=production` for file logging and optimizations diff --git a/.github/instructions/db.instructions.md b/.github/instructions/db.instructions.md new file mode 100644 index 000000000..3d897f1a7 --- /dev/null +++ b/.github/instructions/db.instructions.md @@ -0,0 +1,334 @@ +--- +applyTo: '**' +--- +# Database Structure Overview + +This document describes the project database schema. + +--- + +## Database Architecture Overview + +### Core Concept +This database is designed to support a Formula 1 fantasy game where users can insert votes and compete based on real F1 race results. The schema follows a normalized approach that separates race results from the drivers and teams, allowing for flexible querying and reporting. + +### Result Tables Structure +The database uses a modern **entry-based result system** instead of storing positions as separate columns. This design provides several advantages: + +#### How Result Tables Work: +- **Entry Tables**: Instead of having columns like `1_place_id`, `2_place_id`, etc., we use entry tables that store each driver's result as a separate row +- **Position-Based**: Each entry contains a `position` field (1-6) and references the driver who achieved that position +- **Session Types**: Different session types (Race, Sprint, Qualifying, etc.) have their own entry tables + +#### Result Entry Tables: +1. **`race_result_entries`** - Main race results +2. **`sprint_result_entries`** - Sprint race results +3. **`qualifying_result_entries`** - Qualifying session results +4. **`free_practice_result_entries`** - Free practice session results +5. **`full_race_result_entries`** - Full race results (when `has_x2` flag is set) + +#### Key Benefits: +- **Scalability**: Easy to add more positions or drivers +- **Normalization**: Eliminates redundancy and maintains data integrity +- **Flexibility**: Simple to query for specific positions or driver performance +- **Consistency**: Uniform structure across all session types + +### How to Use Result Tables: + +#### Adding Results: +```sql +-- Example: Adding race results for a Grand Prix +INSERT INTO race_result_entries (race_results_id, pilot_id, position, fast_lap) VALUES +(123, 1, 1, true), -- Driver 1 finished 1st with fastest lap +(123, 2, 2, false), -- Driver 2 finished 2nd +(123, 3, 3, false); -- Driver 3 finished 3rd +``` + +#### Querying Results: +```sql +-- Get all drivers and their positions for a specific race +SELECT d.username, rre.position, rre.fast_lap +FROM race_result_entries rre +JOIN drivers d ON rre.pilot_id = d.id +WHERE rre.race_results_id = 123 +ORDER BY rre.position; +``` + +### Points System +Points are calculated using the `session_type` table which defines point values for each position (1st through 6th) and bonus points for fastest laps. The views automatically calculate total points by joining result entries with session types. + +### Fantasy Game Integration +The `fanta` table stores fantasy team selections, while the result entry tables provide the actual performance data used to calculate fantasy points through the database views. + +--- + +## Table Definitions + +## 1. Cars + +- **id**: Primary key (int8) +- **name**: Car name (text, NOT NULL) +- **overall_score**: Aggregate score for the car (int4, NOT NULL) + +--- + +## 2. Championship + +- **id**: Primary key (int4) +- **gran_prix_id**: Reference to the `gran_prix` table (int4, NOT NULL) + +--- + +## 3. Pilots + +- **id**: Primary key (int8) +- **name**: Pilot's first name +- **surname**: Pilot's surname +- **car_id**: Reference to the `cars` table (int8, NOT NULL) + +--- + +## 4. Seasons + +- **id**: Primary key (serial4) +- **description**: Textual description of the season +- **start_date**: Season start date (timestamp, NOT NULL) +- **end_date**: Season end date (timestamp, nullable) + +--- + + +## 5. Users + +- **id**: Primary key (int4, auto-increment) +- **username**: Username of the user +- **name**: Name of the user +- **surname**: Surname of the user +- **password**: User's password +- **image**: Profile image (bytea) +- **created_at**: Timestamp of account creation (default: NOW()) +- **last_login**: Timestamp of last login +- **password_updated_at**: Timestamp of last password update (default: NOW()) +- **is_active**: Boolean indicating if account is active (default: TRUE) +- **is_admin**: Boolean indicating if user has admin privileges (default: FALSE) + +--- + +## 6. Tracks + +- **id**: Primary key (int8) +- **name**: Track name +- **country**: Track location country +- **length**: Track length (float8, NOT NULL) +- **besttime_driver_time**: Best lap time (text, NOT NULL) +- **besttime_driver_id**: Reference to the best time's driver (int8, nullable) + +--- + +## 7. Gran Prix + +- **id**: Primary key (int8) +- **date**: Date of the event (timestamp, NOT NULL) +- **track_id**: Reference to `tracks` (int8, NOT NULL) +- **race_results_id**: Reference to race result entries (int8, nullable) +- **sprint_results_id**: Reference to sprint result entries (int8, nullable) +- **qualifying_results_id**: Reference to qualifying result entries (int8, nullable) +- **free_practice_results_id**: Reference to free practice result entries (int8, nullable) +- **has_sprint**: Integer flag for sprint existence (int8, nullable) +- **has_x2**: Integer flag for double points or other multiplier (int8, default: 0) +- **full_race_results_id**: Reference to full race result entries (int8, nullable) +- **season_id**: Reference to `seasons` (int4, NOT NULL) + +--- + +## 8. Race Result Entries + +- **race_results_id**: Reference to gran_prix race results (int4, NOT NULL) +- **pilot_id**: Reference to `drivers` (int4, NOT NULL) +- **position**: Position in race (int4, NOT NULL) +- **fast_lap**: Boolean indicating fastest lap (default: false) +- **Primary Key**: Composite (race_results_id, pilot_id) + +--- + +## 9. Sprint Result Entries + +- **sprint_results_id**: Reference to gran_prix sprint results (int4, NOT NULL) +- **pilot_id**: Reference to `drivers` (int4, NOT NULL) +- **position**: Position in sprint (int4, NOT NULL) +- **fast_lap**: Boolean indicating fastest lap (default: false) +- **Unique Constraint**: (sprint_results_id, pilot_id) + +--- + +## 10. Full Race Result Entries + +- **race_results_id**: Reference to gran_prix full race results (int4, NOT NULL) +- **pilot_id**: Reference to `drivers` (int4, NOT NULL) +- **position**: Position in full race (int4, NOT NULL) +- **fast_lap**: Boolean indicating fastest lap (default: false) +- **Primary Key**: Composite (race_results_id, pilot_id) + +--- + +## 11. Qualifying Result Entries + +- **qualifying_results_id**: Reference to gran_prix qualifying results (int4, NOT NULL) +- **pilot_id**: Reference to `drivers` (int4, NOT NULL) +- **position**: Position in qualifying (int4, NOT NULL) +- **Primary Key**: Composite (qualifying_results_id, pilot_id) + +--- + +## 12. Free Practice Result Entries + +- **free_practice_results_id**: Reference to gran_prix free practice results (int4, NOT NULL) +- **pilot_id**: Reference to `drivers` (int4, NOT NULL) +- **position**: Position in free practice (int4, NOT NULL) +- **Primary Key**: Composite (free_practice_results_id, pilot_id) + +--- + +## 13. Fanta + +- **id**: Primary key (int4, auto-increment) +- **fanta_player_id**: Reference to `users` (int8, NOT NULL) +- **race_id**: Reference to a race event (int8, NOT NULL) +- **1_place_id** to **8_place_id**: Fantasy team positions (drivers, int8, NOT NULL) +- **fast_lap_id**: Fastest lap (driver id, int8, NOT NULL) +- **dnf_id**: Driver who did not finish (driver id, int8, NOT NULL) +- **season_id**: Reference to `seasons` (int4, NOT NULL) +- **team_id**: Reference to `teams` (int8, NOT NULL) + +--- + +## 14. Drivers + +- **id**: Primary key (int8) +- **username**: Unique username (text, NOT NULL) +- **name**: Driver's first name +- **surname**: Driver's surname +- **free_practice_points**: Points from free practice (int4, default: 0, NOT NULL) +- **qualifying_points**: Points from qualifying (int4, default: 0, NOT NULL) +- **race_points**: Points from races (int4, default: 0, NOT NULL) +- **pilot_id**: Reference to `pilots` (int8, nullable) +- **description**: Text description +- **consistency_pt**: Consistency points (int4) +- **fast_lap_pt**: Fastest lap points (int4) +- **dangerous_pt**: Dangerous driving points (int4) +- **ingenuity_pt**: Ingenuity points (int4) +- **strategy_pt**: Strategy points (int4) +- **color**: Color or team color +- **license_pt**: License points (int8, default: 3) +- **season**: Reference to `seasons` (int4, NOT NULL) + +--- + +## 15. Constructors + +- **id**: Primary key (int8) +- **name**: Constructor/team name (text, NOT NULL) +- **driver_id_1**: Reference to first driver in the constructor references `drivers(id)` (int8, nullable) +- **driver_id_2**: Reference to second driver in the constructor references `drivers(id)` (int8, nullable) +- **color**: Constructor team color (text, nullable) + +--- + +## 16. Session Type + +- **id**: Primary key (int8) +- **1_points** to **6_points**: Points assigned for each placement (int4, NOT NULL) +- **name**: Name of the session type (text, NOT NULL) - Race, Qualifying, etc. +- **fast_lap_points**: Points for fastest lap (int4, nullable) + +--- + + +## 17. User Sessions + +- **id**: Primary key (serial4) +- **user_id**: Reference to `users (int4, nullable) +- **session_token**: Unique session token (VARCHAR 255, NOT NULL) +- **created_at**: Session creation timestamp (default: NOW()) +- **last_activity**: Last activity timestamp (default: NOW()) +- **expires_at**: Session expiration timestamp (NOT NULL) +- **ip_address**: Client IP address (INET type, nullable) +- **user_agent**: Client user agent string +- **is_active**: Boolean indicating if session is active (default: TRUE) + +--- + +## 18. Property + +- **id**: Primary key (serial4, auto-increment) +- **name**: Unique property name identifier (VARCHAR 255, NOT NULL, UNIQUE) +- **description**: Human-readable description of the property (text, nullable) +- **value**: Property value stored as string (VARCHAR 500, NOT NULL) +- **created_at**: Timestamp when property was created (default: NOW()) +- **updated_at**: Timestamp when property was last updated (default: NOW()) + +**Purpose**: Stores application configuration properties and feature flags that can be modified without code deployment. + +**Common Use Cases**: +- Feature toggles (e.g., enabling/disabling cron jobs) +- Application settings +- Dynamic configuration values + +**Index**: Indexed on `name` column for fast lookups + +**Example Properties**: +- `send_incoming_race_mail_enabled`: Controls the automated email notification cron job (1=enabled, 0=disabled) + +--- + +## Database Views + +The database includes several important views for data analysis: + +### 1. driver_grand_prix_points +Comprehensive view that aggregates points from all session types (Race, Sprint, Qualifying, Free Practice, Full Race) per driver per Grand Prix. Includes: +- Grand Prix details (ID, date, track name, season) +- Driver details (ID, name, username) +- Points breakdown by session type +- Total points calculation + +### 2. driver_grand_prix_points_new +Extended version of driver_grand_prix_points with additional driver attributes: +- Driver statistics (license_pt, consistency_pt, fast_lap_pt, dangerous_pt, ingenuity_pt, strategy_pt) +- Car information (name, overall_score) +- Pilot information (name, surname) +- Session-specific point totals + +### 3. season_driver_leaderboard +Season-wide leaderboard showing total points per driver: +- Season ID and driver information +- Total points across all Grand Prix in the season +- Ordered by season and total points + +### 4. all_race_points +Comprehensive driver statistics view including: +- Driver and pilot information +- Car details +- Points from all session types +- Total points calculation across all sessions + +### 5. all_race_points_v2 +Simplified view showing race points by result type and driver. + +--- + +## Relationships & Notes + +- Result entry tables (race, sprint, qualifying, free practice, full race) store individual driver positions and results per session. +- `gran_prix` acts as a central event, linking to different result entry tables and tracks. +- The schema supports F1-style events with multiple session types per Grand Prix. +- Fantasy game logic is supported through `fanta` and `users` tables. +- Points logic is centralized in `session_type`, supporting flexible point assignment per session (1st through 6th place plus fast lap points). +- The design enables tracking performance and statistics for both real drivers and fantasy teams. +- **Constructor Teams**: The `constructors` table manages F1 team structures, linking two drivers per constructor with team branding (name and color). +- **Result Structure**: Instead of storing positions as separate columns, the new structure uses entry tables with position fields for better normalization. +- **Session Management**: Sessions are managed through the `user_sessions` table with automatic cleanup capabilities. +- **Configuration Management**: The `property` table provides a flexible key-value store for application configuration and feature flags, allowing dynamic control of features like cron jobs without code deployment. +- **Data Integrity**: Foreign key constraints ensure data consistency across related tables. + +--- diff --git a/.github/instructions/project.instructions.md b/.github/instructions/project.instructions.md new file mode 100644 index 000000000..4230a03f3 --- /dev/null +++ b/.github/instructions/project.instructions.md @@ -0,0 +1,167 @@ +--- +applyTo: '**' +--- + +# Project Code Generation Instructions + +## TypeScript Best Practices +- Use strict type checking +- Prefer type inference when the type is obvious +- Avoid the `any` type; use `unknown` when type is uncertain + +## Angular Best Practices +- Always use standalone components over NgModules +- Must NOT set `standalone: true` inside Angular decorators. It's the default in Angular v20+. +- Use signals for state management +- Implement lazy loading for feature routes +- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead +- Use `NgOptimizedImage` for all static images. + - `NgOptimizedImage` does not work for inline base64 images. + +## Accessibility Requirements +- It MUST pass all AXE checks. +- It MUST follow all WCAG AA minimums, including focus management, color contrast, and ARIA attributes. + +## Components +- Keep components small and focused on a single responsibility +- Use `input()` and `output()` functions instead of decorators +- Use `computed()` for derived state +- Set `changeDetection: ChangeDetectionStrategy.OnPush` in `@Component` decorator +- Prefer inline templates for small components +- Prefer Reactive forms instead of Template-driven ones +- Do NOT use `ngClass`, use `class` bindings instead +- Do NOT use `ngStyle`, use `style` bindings instead +- When using external templates/styles, use paths relative to the component TS file +- Place view components in `src/app/views//` +- Place reusable UI components in `src/components/` +- Use `templateUrl` and `styleUrl` for external HTML and SCSS files, named after the component +- Use `OnInit` lifecycle hook for initialization logic +- Use `RouterLink` for navigation +- Use PascalCase for component class names and kebab-case for filenames +- Import all required modules and directives explicitly in the `imports` array +- Name event handlers for what they do, not for the triggering event +- Keep lifecycle methods simple + +## State Management +- Use signals for local component state +- Use `computed()` for derived state +- Keep state transformations pure and predictable +- Do NOT use `mutate` on signals, use `update` or `set` instead + +## Templates +- Keep templates simple and avoid complex logic +- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch` +- Use the async pipe to handle observables +- Do not assume globals like (`new Date()`) are available +- Do not write arrow functions in templates (they are not supported) + +## Services +- Place new services in `src/app/service/` +- Design services around a single responsibility +- Use the `providedIn: 'root'` option for singleton services +- Use the `inject()` function instead of constructor injection +- Use `ApiService` for all API calls to the Express backend (never use `HttpClient` directly) +- Use RxJS for asynchronous operations and state management +- Name services with the `Service` suffix (e.g., `UserService`) +- Import shared types for API from `@f123dashboard/shared` package + +## Routing +- Add new routes in `src/app/app.routes.ts` and/or feature-specific `routes.ts` files +- Use lazy loading for feature modules/components +- Use route `data` for titles and metadata +- Define routes in a `routes.ts` file within the feature folder +- Use lazy loading with `loadComponent: () => import('./feature.component').then(m => m.FeatureComponent)` +- Add route metadata (e.g., `data: { title: $localize`Feature` }`) + +## Testing +- Place unit tests alongside the code, using the `.spec.ts` suffix +- Use Angular's `TestBed` for setting up tests +- Test creation and basic logic for all components and services +- Mock dependencies and services as needed + +## Folder Structure +- Follow the existing structure: + - `src/app/views/` for main app views + - `src/app/service/` for services + - `src/components/` for reusable components + - `src/app/icons/` for icon sets + - `src/assets/` for images and static assets + - `src/scss/` for global styles + +## Style +- Use SCSS for all styles +- Use CoreUI and project SCSS variables and mixins +- Keep styles modular and component-scoped +- Use the `app-` prefix for selectors + +## General +- Use CoreUI components and directives where possible (see imports in existing components) +- Use Angular CLI commands for scaffolding (e.g., `ng generate component`, `ng generate service`) +- Use `ApiService` for all HTTP API calls to the Express backend +- Import types from `@f123dashboard/shared` package for type definitions +- Use constants and do not repeat yourself +- Make the code simple to read with clear variable and method names +- Comments should be avoided and used only to explain public class/method/variable with TSDoc standard +- Use Guard Clause to reduce nesting and improve readability + +## Naming Conventions +- Use English for all code, comments, and documentation +- Use descriptive, self-explanatory names for variables, methods, and classes +- Use PascalCase for class names +- Use camelCase for variables and methods +- Use kebab-case for file and folder names (e.g., `leaderboard.component.ts`) + +## Internationalization +- Use Angular `$localize` for all user-facing strings + +## Example CLI Commands +- Generate a component: `ng generate component app/views/example --standalone --style=scss` +- Generate a service: `ng generate service app/service/example` + +## References +- [CoreUI Angular Docs](https://coreui.io/angular/docs/) +- [Angular CLI Docs](https://angular.io/cli) + +## Backend Architecture +- **Express Backend**: `server/` contains the Express.js backend + - `server/src/services/` - Business logic services + - `server/src/controllers/` - Request/response handlers + - `server/src/routes/` - API route definitions + - `server/src/middleware/` - Authentication and other middleware + - `server/src/config/` - Database and configuration files +- **Shared Types**: `shared/` contains TypeScript type definitions + - `shared/src/models/` - Type definitions (auth, database, fanta, playground, twitch) + - `shared/src/index.ts` - Barrel export for all types + - Both client and server import from `@f123dashboard/shared` +- **Angular Client**: `client/` contains the Angular application + - Uses `ApiService` for all HTTP calls to Express backend + - Imports types from `@f123dashboard/shared` + +## API Communication +- **Development**: Angular dev server proxies `/api/*` requests to `http://localhost:3000` +- **Production**: Express serves the built Angular app and handles `/api/*` routes +- **Endpoints**: All API routes are prefixed with `/api/` (e.g., `/api/database/drivers`, `/api/auth/login`) +- **Authentication**: JWT tokens passed via `Authorization: Bearer ` header +- Use `ApiService.post()`, `ApiService.get()`, etc. for type-safe API calls + +## Type Definitions +- **Never** define types inline in services or components +- **Always** import types from `@f123dashboard/shared` +- Example: `import type { User, DriverData, FantaVote } from '@f123dashboard/shared';` +- Use `type` imports for better tree-shaking and clarity + +## Development Workflow +- **Shared Package**: Run `npm run build` in `shared/` after modifying types +- **Server**: Run `npm run dev` in `server/` for development with auto-reload +- **Client**: Run `npm start` in `client/` for Angular dev server with proxy +- **Full Build**: Run `npm run build` from root to build all packages in order + +## Commit Message Convention +- Follow the conventional commit message format as described in `.github/COMMIT_CONVENTION.md` +- Use a clear header with type, optional scope, and subject: `(): ` +- Types include: `feat`, `fix`, `perf`, `docs`, `chore`, `style`, `refactor`, `test`, and `revert` +- Use the imperative, present tense in the subject (e.g., "add feature" not "added feature") +- Reference issues and breaking changes in the footer as needed +- See the `.github/COMMIT_CONVENTION.md` file for detailed examples and rules + +--- diff --git a/.github/prompts/plan-frontendMigration.prompt.md b/.github/prompts/plan-frontendMigration.prompt.md new file mode 100644 index 000000000..3a04949e3 --- /dev/null +++ b/.github/prompts/plan-frontendMigration.prompt.md @@ -0,0 +1,1045 @@ +# Plan: Angular Frontend Migration to Express.js Backend + +This document provides a comprehensive roadmap for migrating the Angular frontend from Genezio SDK to the new Express.js backend API. + +## Current State Analysis + +### Genezio Dependencies Found: +- **Package**: `@genezio-sdk/f123dashboard` - Used throughout the application +- **Services Using Genezio**: + - `auth.service.ts` - Uses `AuthService` from Genezio SDK + - `db-data.service.ts` - Uses `PostgresService`, `FantaService` from Genezio SDK + - `fanta.service.ts` - Uses Genezio types + - `season.service.ts` - Uses `PostgresService`, `Season` from Genezio SDK + - `twitch-api.service.ts` - Uses `DreandosTwitchInterface` from Genezio SDK + - `playground.service.ts` - Uses `PlaygroundInterface` from Genezio SDK + +### Components Using Genezio Types: +- Multiple components import types from `@genezio-sdk/f123dashboard` +- These will need to import from local type definitions instead + +## Migration Goals + +1. **Remove Genezio Dependency**: Eliminate `@genezio-sdk/f123dashboard` and `genezio` packages +2. **Create Shared Types Package**: Single source of truth for types used by both frontend and backend +3. **Update Environment Configuration**: Configure API URLs for dev and production +4. **Setup Proxy Configuration**: Handle `/api/*` requests in development +5. **Refactor Services**: Update all services to use HttpClient instead of Genezio SDK +6. **Concurrent Development**: Enable running frontend and backend simultaneously +7. **Update Build Process**: Ensure production builds work correctly + +--- + +## Phase 1: Environment & Configuration Setup + +### 1.1 Create Proxy Configuration for Development + +**File**: `client/proxy.conf.json` +```json +{ + "/api": { + "target": "http://localhost:3000", + "secure": false, + "changeOrigin": true, + "logLevel": "debug" + } +} +``` + +**Purpose**: Redirect all `/api/*` requests to the Express backend during development, avoiding CORS issues. + +### 1.2 Update Angular Configuration + +**File**: `client/angular.json` + +Add proxy configuration to the development server options: +```json +"serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "proxyConfig": "proxy.conf.json" + } +} +``` + +### 1.3 Update Environment Files + +**File**: `client/src/environments/environment.ts` (Development) +```typescript +export const environment = { + production: false, + apiBaseUrl: '/api' // Will be proxied to http://localhost:3000/api +}; +``` + +**File**: `client/src/environments/environment.prod.ts` (Production) +```typescript +export const environment = { + production: true, + apiBaseUrl: '/api' // Same origin in production (Express serves both) +}; +``` + +**Rationale**: Using `/api` as a relative path allows the same configuration to work in both development (via proxy) and production (same server). + +--- + +## Phase 2: Shared Types Package (Recommended Approach) + +### 2.0 Why Shared Package? + +**Benefits**: +- ✅ **Single Source of Truth**: Types defined once, used by both frontend and backend +- ✅ **Type Safety Across Stack**: Compile-time errors if frontend/backend types drift +- ✅ **Easy Maintenance**: Update types in one place, both apps get the changes +- ✅ **Scalable**: Easy to add more consumers (mobile app, CLI tools, etc.) +- ✅ **No Duplication**: Zero code duplication between client and server + +### 2.1 Create Shared Package Structure + +**File**: `shared/package.json` +```json +{ + "name": "@f123dashboard/shared", + "version": "1.0.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "watch": "tsc --watch" + }, + "devDependencies": { + "typescript": "^5.6.3" + } +} +``` + +**File**: `shared/tsconfig.json` +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +### 2.2 Extract Types from Server Services + +Copy exact type definitions from `server/src/services/*.ts` to the shared package: + +**File**: `shared/src/models/auth.ts` + +```typescript +// Extracted from server/src/services/auth.service.ts + +export type User = { + id: number; + username: string; + name: string; + surname: string; + mail?: string; + image?: string; + isAdmin?: boolean; +} + +export type LoginRequest = { + username: string; + password: string; + userAgent?: string; +} + +export type RegisterRequest = { + username: string; + name: string; + surname: string; + password: string; + mail: string; + image: string; +} + +export type AuthResponse = { + success: boolean; + message: string; + user?: User; + token?: string; +} + +export type ChangePasswordRequest = { + currentPassword: string; + newPassword: string; + jwtToken: string; +} + +export type AdminChangePasswordRequest = { + userName: string; + newPassword: string; + jwtToken: string; +} + +export type ChangePasswordResponse = { + success: boolean; + message: string; +} + +export type TokenValidationResponse = { + valid: boolean; + userId?: number; + username?: string; + name?: string; + surname?: string; + mail?: string; + image?: string; + isAdmin?: boolean; +} + +export type RefreshTokenResponse = { + success: boolean; + token?: string; + message: string; +} + +export type LogoutResponse = { + success: boolean; + message: string; +} + +export type UpdateUserInfoRequest = { + name?: string; + surname?: string; + mail?: string; + image?: string; + jwt: string; +} + +export type UserSession = { + sessionToken: string; + createdAt: string; + lastActivity: string; + expiresAt: string; + isActive: boolean; + isCurrent: boolean; +} + +export type SessionsResponse = { + success: boolean; + message?: string; + sessions?: UserSession[]; +} +``` + +**File**: `shared/src/models/database.ts` +```typescript +// Extracted from server/src/services/database.service.ts + +export type DriverData = { + driver_id: number; + driver_username: string; + driver_name: string; + driver_surname: string; + driver_description: string; + driver_license_pt: number; + driver_consistency_pt: number; + driver_fast_lap_pt: number; + drivers_dangerous_pt: number; + driver_ingenuity_pt: number; + driver_strategy_pt: number; + driver_color: string; + car_name: string; + car_overall_score: number; + total_sprint_points: number; + total_free_practice_points: number; + total_qualifying_points: number; + total_full_race_points: number; + total_race_points: number; + total_points: number; +} + +export type Driver = { + id: number; + username: string; + first_name: string; + surname: string; +} + +export type SessionResult = { + position: number; + driver_username: string; + fast_lap: boolean | null; +} + +export type ChampionshipData = { + gran_prix_id: number; + track_name: string; + gran_prix_date: Date; + gran_prix_has_sprint: number; + gran_prix_has_x2: number; + track_country: string; + sessions: { + free_practice?: SessionResult[]; + qualifying?: SessionResult[]; + race?: SessionResult[]; + sprint?: SessionResult[]; + full_race?: SessionResult[]; + }; + fastLapDrivers: { + race?: string; + sprint?: string; + full_race?: string; + }; +} + +export type Season = { + id: number; + description: string; + startDate?: Date; + endDate?: Date; +} + +export type TrackData = { + track_id: number; + name: string; + date: string; + has_sprint: number; + has_x2: number; + country: string; + besttime_driver_time: string; + username: string; +} + +export type CumulativePointsData = { + date: string; + track_name: string; + driver_id: number; + driver_username: string; + driver_color: string; + cumulative_points: number; +} + +export type RaceResult = { + id: number; + track_id: number; + id_1_place: number; + id_2_place: number; + id_3_place: number; + id_4_place: number; + id_5_place: number; + id_6_place: number; + id_7_place: number; + id_8_place: number; + id_fast_lap: number; + list_dnf: string; +} + +export type Constructor = { + constructor_id: number; + constructor_name: string; + constructor_color: string; + driver_1_id: number; + driver_1_username: string; + driver_1_tot_points: number; + driver_2_id: number; + driver_2_username: string; + driver_2_tot_points: number; + constructor_tot_points: number; + constructor_race_points?: number; + constructor_full_race_points?: number; + constructor_sprint_points?: number; + constructor_qualifying_points?: number; + constructor_free_practice_points?: number; +} + +export type ConstructorGrandPrixPoints = { + constructor_id: number; + constructor_name: string; + grand_prix_id: number; + grand_prix_date: string; + track_name: string; + track_id: number; + season_id: number; + season_description: string; + driver_id_1: number; + driver_id_2: number; + driver_1_points: number; + driver_2_points: number; + constructor_points: number; +} +``` + +**File**: `shared/src/models/fanta.ts` +```typescript +// Extracted from server/src/services/fanta.service.ts + +export type FantaVote = { + fanta_player_id: number; + username: string; + track_id: number; + id_1_place: number; + id_2_place: number; + id_3_place: number; + id_4_place: number; + id_5_place: number; + id_6_place: number; + id_7_place: number; + id_8_place: number; + id_fast_lap: number; + id_dnf: number; + season_id?: number; + constructor_id: number; +} +``` + +**File**: `shared/src/models/playground.ts` +```typescript +// Extracted from server/src/services/playground.service.ts + +export type PlaygroundBestScore = { + user_id: number; + username: string; + image: string; + best_score: number; + best_date: Date; +} +``` + +**File**: `shared/src/models/twitch.ts` +```typescript +// Extracted from server/src/services/twitch.service.ts + +export type TwitchTokenResponse = { + access_token: string; + expires_in: number; +} + +export type TwitchStreamResponse = { + data: Array<{ + id: string; + user_id: string; + user_login: string; + type: string; + title: string; + viewer_count: number; + started_at: string; + language: string; + thumbnail_url: string; + }>; +} +``` + +**File**: `shared/src/index.ts` +```typescript +// Barrel export for all shared types +export * from './models/auth'; +export * from './models/database'; +export * from './models/fanta'; +export * from './models/playground'; +export * from './models/twitch'; +``` + +### 2.3 Update Root package.json for Workspaces + +**File**: `package.json` (root) +```json +{ + "name": "f123dashboard", + "private": true, + "workspaces": [ + "client", + "server", + "shared" + ], + "scripts": { + "start:client": "npm start --prefix client", + "start:server": "npm run dev --prefix server", + "start:dev": "concurrently \"npm run start:server\" \"npm run start:client\"", + "build:shared": "npm run build --prefix shared", + "build:client": "npm run build --prefix client", + "build:server": "npm run build --prefix server", + "build": "npm run build:shared && npm run build:server && npm run build:client", + "test:client": "npm test --prefix client", + "test:server": "npm test --prefix server" + }, + "devDependencies": { + "@playwright/test": "^1.54.2", + "@types/node": "^24.2.1", + "concurrently": "^9.1.0", + "sass": "^1.89.2" + } +} +``` + +### 2.4 Update Server to Use Shared Types + +**File**: `server/package.json` +```json +{ + "dependencies": { + "@f123dashboard/shared": "file:../shared", + // ... other dependencies + } +} +``` + +**Update Server Services** - Example for `server/src/services/auth.service.ts`: + +**Before**: +```typescript +export type User = { + id: number; + username: string; + // ... rest of type +} +// ... all other type definitions +``` + +**After**: +```typescript +import type { + User, + LoginRequest, + RegisterRequest, + AuthResponse, + ChangePasswordRequest, + AdminChangePasswordRequest, + ChangePasswordResponse, + TokenValidationResponse, + RefreshTokenResponse, + LogoutResponse, + UpdateUserInfoRequest, + UserSession, + SessionsResponse +} from '@f123dashboard/shared'; + +// Remove all type definitions - they're now imported from shared +export class AuthService { + // Implementation unchanged +} +``` + +### 2.5 Update Client to Use Shared Types + +**File**: `client/package.json` +```json +{ + "dependencies": { + "@f123dashboard/shared": "file:../shared", + // ... other dependencies + } +} +``` + +**File**: `client/tsconfig.app.json` +```json +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [], + "paths": { + "@f123dashboard/shared": ["../shared/src"] + } + } +} +``` + +**Update Angular Services** - Example for `client/src/app/service/auth.service.ts`: + +**Before**: +```typescript +import { User, LoginRequest, AuthResponse } from '@genezio-sdk/f123dashboard'; +``` + +**After**: +```typescript +import type { User, LoginRequest, AuthResponse } from '@f123dashboard/shared'; +``` + +**Update Angular Components**: + +**Before**: +```typescript +import { User, FantaVote } from '@genezio-sdk/f123dashboard'; +``` + +**After**: +```typescript +import type { User, FantaVote } from '@f123dashboard/shared'; +``` + +--- + +## Phase 3: Service Migration + +### 3.1 Update AuthService + +**File**: `client/src/app/service/auth.service.ts` + +**Changes**: +1. Remove Genezio imports +2. Import types from local model +3. Update all SDK calls to use `ApiService` with HttpClient +4. Update API endpoints to match Express routes + +**Example refactoring**: +```typescript +// Before: +import { AuthService as BackendAuthService } from "@genezio-sdk/f123dashboard"; +const response = await BackendAuthService.login(loginRequest); + +// After: +import type { LoginRequest, AuthResponse } from '@f123dashboard/shared'; +const response = await firstValueFrom( + this.apiService.post('/auth/login', loginRequest) +); +``` + +**API Endpoints Mapping**: +- `login()` → `POST /api/auth/login` +- `register()` → `POST /api/auth/register` +- `validateToken()` → `POST /api/auth/validate` +- `refreshToken()` → `POST /api/auth/refresh` +- `logout()` → `POST /api/auth/logout` +- `changePassword()` → `POST /api/auth/change-password` +- `updateUserInfo()` → `PUT /api/auth/update-info` +- `getUserSessions()` → `GET /api/auth/sessions` +- `terminateSession()` → `DELETE /api/auth/sessions/:sessionId` +- `terminateAllOtherSessions()` → `DELETE /api/auth/sessions/others` + +### 3.2 Update DbDataService + +**File**: `client/src/app/service/db-data.service.ts` + +**Changes**: +1. Remove Genezio imports +2. Import types from local model +3. Inject `ApiService` +4. Update all `PostgresService` calls to HTTP requests + +**Example refactoring**: +```typescript +// Before: +import { PostgresService } from "@genezio-sdk/f123dashboard"; +const drivers = await PostgresService.getAllDrivers(); + +// After: +import type { DriverData } from '@f123dashboard/shared'; +import { ApiService } from './api.service'; + +private apiService = inject(ApiService); + +async getAllDrivers(): Promise { + return firstValueFrom( + this.apiService.get('/database/drivers') + ); +} +``` + +**API Endpoints Mapping**: +- `getAllDrivers()` → `GET /api/database/drivers` +- `getChampionship()` → `GET /api/database/championship` +- `getCumulativePoints()` → `GET /api/database/cumulative-points` +- `getAllTracks()` → `GET /api/database/tracks` +- `getRaceResults()` → `GET /api/database/race-results` +- `getUsers()` → `GET /api/database/users` +- `getConstructors()` → `GET /api/database/constructors` +- `getConstructorGrandPrixPoints()` → `GET /api/database/constructor-points` +- `getSeasons()` → `GET /api/database/seasons` + +### 3.3 Update FantaService + +**File**: `client/src/app/service/fanta.service.ts` + +**Changes**: +1. Remove Genezio type imports +2. Import types from local model +3. Update to use `ApiService` for any direct backend calls + +**API Endpoints Mapping** (if needed): +- `getFantaVotes()` → `GET /api/fanta/votes` +- `submitFantaVote()` → `POST /api/fanta/votes` +- `updateFantaVote()` → `PUT /api/fanta/votes/:id` +- `deleteFantaVote()` → `DELETE /api/fanta/votes/:id` + +### 3.4 Update SeasonService + +**File**: `client/src/app/service/season.service.ts` + +**Changes**: +1. Remove Genezio imports +2. Import types from local model +3. Update to use `ApiService` + +**Example**: +```typescript +// Before: +import { PostgresService, Season } from "@genezio-sdk/f123dashboard"; +this.seasons = await PostgresService.getSeasons(); + +// After: +import type { Season } from '@f123dashboard/shared'; +import { ApiService } from './api.service'; + +this.seasons = await firstValueFrom( + this.apiService.get('/database/seasons') +); +``` + +### 3.5 Update TwitchApiService + +**File**: `client/src/app/service/twitch-api.service.ts` + +**Changes**: +1. Remove Genezio imports +2. Import types from local model +3. Update to use `ApiService` + +**API Endpoints Mapping**: +- `getStreamStatus()` → `GET /api/twitch/stream/dreandos` + +### 3.6 Update PlaygroundService + +**File**: `client/src/app/service/playground.service.ts` + +**Changes**: +1. Remove Genezio imports +2. Import types from local model +3. Update to use `ApiService` + +**API Endpoints Mapping**: +- `getBestScores()` → `GET /api/playground/best-scores` +- `submitScore()` → `POST /api/playground/scores` + +--- + +## Phase 4: Component Updates + +### 4.1 Update All Component Imports + +Replace all imports of Genezio types with shared package imports: + +```typescript +// Before: +import { User, FantaVote } from '@genezio-sdk/f123dashboard'; + +// After: +import type { User, FantaVote } from '@f123dashboard/shared'; +``` + +**Components to Update**: +- `admin.component.ts` +- `admin-change-password.component.ts` +- `albo-d-oro.component.ts` +- `championship.component.ts` +- `dashboard.component.ts` +- `dashboard-charts-data.ts` +- `fanta.component.ts` +- `piloti.component.ts` +- `playground.component.ts` +- `leaderboard.component.ts` +- `login.component.ts` +- `registration-modal.component.ts` +- `vote-history-table.component.ts` + +--- + +## Phase 5: Package Management + +### 5.1 Update Root package.json + +**File**: `package.json` (root) + +Update scripts to run both frontend and backend: + +```json +{ + "scripts": { + "start:client": "npm start --prefix client", + "start:server": "npm run dev --prefix server", + "start:dev": "concurrently \"npm run start:server\" \"npm run start:client\"", + "build:client": "npm run build --prefix client", + "build:server": "npm run build --prefix server", + "build": "npm run build:server && npm run build:client", + "test:client": "npm test --prefix client", + "test:server": "npm test --prefix server" + }, + "devDependencies": { + "concurrently": "^9.1.0" + } +} +``` + +**Install concurrently**: +```bash +npm install --save-dev concurrently +``` + +### 5.2 Update Client package.json + +**File**: `client/package.json` + +**Remove**: +```json +"@genezio-sdk/f123dashboard": "^1.0.0-prod", +"genezio": "^2.6.3" +``` + +**Update Scripts**: +```json +{ + "scripts": { + "start": "ng serve -o --host 0.0.0.0 --proxy-config proxy.conf.json", + "build": "ng build --configuration production", + "build:dev": "ng build --configuration development" + } +} +``` + +--- + +## Phase 6: Build & Deployment Configuration + +### 6.1 Update Angular Build Configuration + +**File**: `client/angular.json` + +Ensure the production build outputs to `dist/browser` (as referenced in Express server): + +```json +"build": { + "builder": "@angular/build:application", + "options": { + "outputPath": "dist", + "index": "src/index.html", + "browser": "src/main.ts", + ... + } +} +``` + +### 6.2 Verify Express Static File Serving + +**File**: `server/src/server.ts` + +Ensure Express serves Angular in production: + +```typescript +if (process.env.NODE_ENV === 'production') { + const angularDistPath = path.join(__dirname, '../../../client/dist/browser'); + app.use(express.static(angularDistPath)); + + app.get('*', (req: Request, res: Response) => { + res.sendFile(path.join(angularDistPath, 'index.html')); + }); +} +``` + +--- + +## Phase 7: Testing & Validation + +### 7.1 Development Testing + +1. **Start Backend**: + ```bash + cd server + npm run dev + ``` + +2. **Start Frontend** (in separate terminal): + ```bash + cd client + npm start + ``` + +3. **Or use concurrent script** (from root): + ```bash + npm run start:dev + ``` + +4. **Verify**: + - Frontend opens at `http://localhost:4200` + - API requests proxy to `http://localhost:3000/api/*` + - No CORS errors + - Authentication works + - All data loads correctly + +### 7.2 Production Build Testing + +1. **Build Both**: + ```bash + npm run build + ``` + +2. **Start Production Server**: + ```bash + cd server + NODE_ENV=production npm start + ``` + +3. **Verify**: + - Server runs on configured port + - Angular app loads from Express + - API requests work on same origin + - All routes work (Angular routing handled correctly) + +--- + +## Migration Checklist + +### Phase 1: Configuration ✓ +- [ ] **1.1**: Create `client/proxy.conf.json` for development proxy +- [ ] **1.2**: Update `client/angular.json` to use proxy configuration +- [ ] **1.3**: Update environment files (`environment.ts`, `environment.prod.ts`) + +### Phase 2: Shared Types Package ✓ +- [ ] **2.1**: Create `shared/` directory structure +- [ ] **2.2**: Create `shared/package.json` and `shared/tsconfig.json` +- [ ] **2.3**: Extract auth types to `shared/src/models/auth.ts` +- [ ] **2.4**: Extract database types to `shared/src/models/database.ts` +- [ ] **2.5**: Extract fanta types to `shared/src/models/fanta.ts` +- [ ] **2.6**: Extract playground types to `shared/src/models/playground.ts` +- [ ] **2.7**: Extract twitch types to `shared/src/models/twitch.ts` +- [ ] **2.8**: Create barrel export `shared/src/index.ts` +- [ ] **2.9**: Update root `package.json` with workspaces config +- [ ] **2.10**: Build shared package (`cd shared && npm run build`) +- [ ] **2.11**: Update `server/package.json` to depend on `@f123dashboard/shared` +- [ ] **2.12**: Update server services to import from `@f123dashboard/shared` +- [ ] **2.13**: Remove type definitions from server services +- [ ] **2.14**: Update `client/package.json` to depend on `@f123dashboard/shared` +- [ ] **2.15**: Update `client/tsconfig.app.json` with path mapping +- [ ] **2.16**: Update Angular services to import from `@f123dashboard/shared` +- [ ] **2.17**: Update Angular components to import from `@f123dashboard/shared` + +### Phase 3: Service Migration ✓ +- [ ] **3.1**: Refactor `auth.service.ts` to use HttpClient +- [ ] **3.2**: Refactor `db-data.service.ts` to use HttpClient +- [ ] **3.3**: Refactor `fanta.service.ts` to use HttpClient +- [ ] **3.4**: Refactor `season.service.ts` to use HttpClient +- [ ] **3.5**: Refactor `twitch-api.service.ts` to use HttpClient +- [ ] **3.6**: Refactor `playground.service.ts` to use HttpClient + +### Phase 4: Component Updates ✓ +- [ ] **4.1**: Update all component imports (14 components identified) + +### Phase 5: Package Management ✓ +- [ ] **5.1**: Update root `package.json` with workspaces and concurrent scripts +- [ ] **5.2**: Install `concurrently` package +- [ ] **5.3**: Remove Genezio dependencies from `client/package.json` +- [ ] **5.4**: Update client scripts for proxy usage +- [ ] **5.5**: Run `npm install` from root to link workspaces + +### Phase 6: Build & Deployment ✓ +- [ ] **6.1**: Verify Angular build configuration +- [ ] **6.2**: Verify Express static file serving + +### Phase 7: Testing ✓ +- [ ] **7.1**: Test development mode with proxy +- [ ] **7.2**: Test concurrent development (frontend + backend) +- [ ] **7.3**: Test production build +- [ ] **7.4**: Test all API endpoints +- [ ] **7.5**: Test authentication flow +- [ ] **7.6**: Test all user-facing features + +--- + +## Expected File Structure After Migration + +``` +f123dashboard/ +├── client/ # Angular app +│ ├── proxy.conf.json # NEW: Development proxy config +│ ├── angular.json # UPDATED: Added proxy config +│ ├── package.json # UPDATED: Removed Genezio, added shared +│ ├── tsconfig.app.json # UPDATED: Path mapping for shared +│ └── src/ +│ ├── environments/ +│ │ ├── environment.ts # UPDATED: Use /api +│ │ └── environment.prod.ts # UPDATED: Use /api +│ └── app/ +│ └── service/ +│ ├── api.service.ts # EXISTING: Already using HttpClient +│ ├── auth.service.ts # UPDATED: Import from @f123dashboard/shared +│ ├── db-data.service.ts # UPDATED: Import from @f123dashboard/shared +│ ├── fanta.service.ts # UPDATED: Import from @f123dashboard/shared +│ ├── season.service.ts # UPDATED: Import from @f123dashboard/shared +│ ├── twitch-api.service.ts # UPDATED: Import from @f123dashboard/shared +│ └── playground.service.ts # UPDATED: Import from @f123dashboard/shared +├── server/ # Express backend +│ ├── package.json # UPDATED: Added shared dependency +│ └── src/ +│ ├── server.ts # EXISTING: Already serves Angular +│ └── services/ +│ ├── auth.service.ts # UPDATED: Import types from @f123dashboard/shared +│ ├── database.service.ts # UPDATED: Import types from @f123dashboard/shared +│ ├── fanta.service.ts # UPDATED: Import types from @f123dashboard/shared +│ ├── playground.service.ts # UPDATED: Import types from @f123dashboard/shared +│ └── twitch.service.ts # UPDATED: Import types from @f123dashboard/shared +├── shared/ # NEW: Shared TypeScript types +│ ├── src/ +│ │ ├── models/ +│ │ │ ├── auth.ts # NEW: Auth types +│ │ │ ├── database.ts # NEW: Database types +│ │ │ ├── fanta.ts # NEW: Fanta types +│ │ │ ├── playground.ts # NEW: Playground types +│ │ │ └── twitch.ts # NEW: Twitch types +│ │ └── index.ts # NEW: Barrel export +│ ├── dist/ # Compiled JavaScript + .d.ts files +│ ├── package.json # NEW: Shared package config +│ └── tsconfig.json # NEW: TypeScript config +└── package.json # UPDATED: Workspaces + concurrent scripts +``` + +--- + +## Key Benefits of This Approach + +1. **No CORS Issues**: Proxy in dev, same origin in prod +2. **Type Safety**: All types defined locally with TypeScript +3. **Independent Development**: Frontend and backend can run simultaneously +4. **Simple Deployment**: Single Express server serves everything +5. **Clean Architecture**: Clear separation of concerns +6. **Easy Testing**: Can test frontend and backend independently +7. **Production Ready**: Optimized build process for deployment + +--- + +## Troubleshooting Guide + +### Issue: Proxy Not Working +- Verify `proxy.conf.json` is in the `client/` directory +- Check `angular.json` has correct proxy config reference +- Restart `ng serve` after config changes + +### Issue: Type Errors After Migration +- Verify all types are defined in `backend-types.ts` +- Check import paths are correct +- Ensure barrel export is working + +### Issue: API Calls Failing +- Verify backend is running on port 3000 +- Check network tab for actual request URLs +- Verify API endpoint paths match backend routes + +### Issue: Production Build Not Serving +- Check `NODE_ENV=production` is set +- Verify Angular dist path in `server.ts` +- Check Express static middleware is configured correctly + +--- + +## Next Steps After Migration + +1. **Update Documentation**: Document new API structure +2. **Setup CI/CD**: Configure GitHub Actions for deployment +3. **Add API Tests**: Create integration tests for API endpoints +4. **Monitoring**: Add logging and monitoring for production +5. **Performance**: Optimize bundle size and API responses diff --git a/.github/prompts/plan-fullBackendMigration.prompt.md b/.github/prompts/plan-fullBackendMigration.prompt.md new file mode 100644 index 000000000..993a95284 --- /dev/null +++ b/.github/prompts/plan-fullBackendMigration.prompt.md @@ -0,0 +1,103 @@ +## Plan: Full Backend Migration to Express.js + +This document provides a complete roadmap for migrating the Genezio-based backend to a self-hosted Express.js server. It covers the initial setup, service refactoring, secure secret management, and deployment strategy. + +### 1. Why Express.js is the Best Choice + +- **Minimal Migration Effort**: Your existing class-based services map almost directly to Express route handlers, minimizing the need for complex refactoring. +- **Ideal for Your Architecture**: The backend is stateless and database-focused, which is a perfect fit for Express's simple request/response model. +- **Simplified Deployment**: Since the Angular frontend and the backend will be on the same machine, Express can serve both, eliminating CORS issues and simplifying the deployment process. + +### 2. Proposed Architecture + +``` +f123dashboard/ +├── client/ # Angular app (unchanged for now) +├── server/ +│ ├── src/ +│ │ ├── server.ts # Express app entry point +│ │ ├── config/ +│ │ │ └── db.ts # Centralized database connection +│ │ ├── routes/ # API routes (e.g., auth.routes.ts) +│ │ ├── controllers/ # Request/response handlers +│ │ ├── services/ # Your existing service logic +│ │ └── middleware/ # Auth, error handling, etc. +│ ├── .env # Local environment variables (ignored by Git) +│ ├── .env.example # Template for environment variables +│ ├── .gitignore # To ignore .env and node_modules +│ └── package.json +└── ... +``` + +### 3. Secure Secret Management + +We will adopt a professional workflow for managing secrets. + +- **Local Development**: Use a `server/.env` file for local database credentials and keys. This file **must** be added to your `.gitignore` to prevent it from ever being committed to your repository. +- **Production & CI/CD**: Use **GitHub Actions Secrets** (`Settings` > `Secrets and variables` > `Actions`). These are encrypted and provide the most secure way to manage production credentials for automated workflows. + +### 4. Step-by-Step Migration Plan + +#### Phase 1: Setup Express Server & Secure Configuration +1. **Initialize Project**: In the `server/` directory, run `npm init -y` and install dependencies: + ```bash + npm install express cors pg jsonwebtoken dotenv + npm install --save-dev @types/express @types/cors @types/pg @types/jsonwebtoken tsx typescript + ``` +2. **Configure Secrets**: + - Update the root `.gitignore` file to ensure it ignores `.env` files by adding the line `server/.env`. + - Create a `server/.env.example` file with placeholder values. +3. **Create Server Entrypoint** (`server/src/server.ts`): Set up the basic Express app, apply middleware, and define a basic health-check route. +4. **Centralize DB Connection** (`server/src/config/db.ts`): Create and export a `pg.Pool` instance that reads its configuration from the environment variables. + +#### Phase 2: Migrate All Services to the New Architecture +For each of the original service files (`auth_interface.ts`, `db_interface.ts`, `fanta_interface.ts`, `twitch_interface.ts`, `playground_interface.ts`), perform the following steps: + +1. **Refactor the Service**: + - Move the file to `server/src/services/` and rename it (e.g., `db_interface.ts` becomes `database.service.ts`). + - Remove the `@GenezioDeploy` decorator. + - Modify the class to accept the database pool via its constructor instead of creating a new one. + +2. **Create the Controller**: + - Create a corresponding controller file in `server/src/controllers/` (e.g., `database.controller.ts`). + - The controller will import the service, handle the Express `req` and `res` objects, and call the appropriate service methods, wrapping the logic in a `try...catch` block for error handling. + +3. **Define the Routes**: + - Create a router file in `server/src/routes/` (e.g., `database.routes.ts`). + - Define the API endpoints (e.g., `/drivers`, `/tracks`) and map them to the controller functions. + - Mount the router in `server.ts` under a base path (e.g., `app.use('/api/database', databaseRouter)`). + +#### Phase 3: Update Angular Frontend +1. **Modify `ApiService`**: Update `client/src/app/service/api.service.ts` to use Angular's `HttpClient`. +2. **Replace Genezio Calls**: Replace all calls from the Genezio SDK with HTTP requests to your new Express endpoints (e.g., `this.api.post('/api/database/drivers', ...)`). + +#### Phase 4: Implement Authentication & Deployment +1. **JWT Middleware**: Create `server/src/middleware/auth.middleware.ts` to protect routes. It will validate the `Authorization` header. +2. **Deployment Workflow**: Create a GitHub Actions workflow (`.github/workflows/deploy.yml`) that: + - Builds the Angular client. + - Builds the Express server. + - Securely connects to your server (e.g., via SSH). + - Injects the GitHub Actions Secrets into a `.env` file on the server. + - Starts or restarts the application using a process manager like `pm2`. + +### 5. Migration Checklist + +Complete the following tasks in order: + +- [X] **Phase 1.1**: Install Express dependencies and TypeScript types +- [X] **Phase 1.2**: Update root `.gitignore` to include `server/.env` +- [X] **Phase 1.3**: Create `server/.env.example` with placeholder values +- [X] **Phase 1.4**: Create `server/src/config/db.ts` for centralized database connection +- [X] **Phase 1.5**: Create `server/src/server.ts` Express app entry point +- [X] **Phase 2.1**: Migrate `db_interface.ts` → `database.service.ts` + controller + routes +- [X] **Phase 2.2**: Migrate `auth_interface.ts` → `auth.service.ts` + controller + routes +- [X] **Phase 2.3**: Create `server/src/middleware/auth.middleware.ts` for JWT validation +- [X] **Phase 2.4**: Migrate `fanta_interface.ts` → `fanta.service.ts` + controller + routes +- [X] **Phase 2.6**: Migrate `playground_interface.ts` → `playground.service.ts` + controller + routes +- [X] **Phase 3.1**: Refactor `client/src/app/service/api.service.ts` to use HttpClient +- [X] **Phase 3.2**: Update all Angular service calls to use new Express API endpoints +- [X] **Phase 2.5**: Migrate `twitch_interface.ts` → `twitch.service.ts` + controller + routes +- [X] **Phase 4.1**: Test all API endpoints locally +- [ ] **Phase 4.2**: Create GitHub Actions workflow for automated deployment +- [ ] **Phase 4.3**: Configure GitHub Actions Secrets for production environment +- [ ] **Phase 4.4**: Deploy and verify production environment diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml deleted file mode 100644 index 4712360b2..000000000 --- a/.github/workflows/build-check.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Build Check - -on: - push: - branches-ignore: - - "dependabot/**" - pull_request: - branches: - - main - - v5.* - schedule: - - # build runs every weekday at 4:15AM UTC - - cron: '15 4 * * *' - -env: - FORCE_COLOR: 2 - NODE: 20 - -jobs: - build: - strategy: - matrix: - platform: [ubuntu-latest, windows-latest, macOS-latest] - node-version: [20.x] - runs-on: ${{ matrix.platform }} - steps: - - name: Clone repository - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.node-version }} - - - name: Install npm dependencies - run: npm install - - - name: Run build - run: npm run build diff --git a/.github/workflows/build-check_and_deply.yml b/.github/workflows/build-check_and_deply.yml new file mode 100644 index 000000000..54320e7cc --- /dev/null +++ b/.github/workflows/build-check_and_deply.yml @@ -0,0 +1,60 @@ +name: Build Check and Deploy + +on: + push: + branches-ignore: + - "dependabot/**" + pull_request: + types: [closed] + branches: + - main + - v5.* + schedule: + - cron: '15 4 * * *' + +env: + FORCE_COLOR: 2 + NODE: 24 + +jobs: + build: + strategy: + matrix: + platform: [ubuntu-latest] + node-version: [24.x] + runs-on: ${{ matrix.platform }} + steps: + - name: Clone repository + uses: actions/checkout@v5 + + - name: Set up Node.js + uses: actions/setup-node@v5 + with: + node-version: ${{ matrix.node-version }} + + - name: Install npm dependencies + run: npm install + + - name: Build Shared Package + run: npm run build --prefix shared + + - name: Run Backend Tests + run: npm run test --prefix server + + - name: Run Angular Tests + run: npm run test --prefix client -- --watch=false --browsers=ChromeHeadless + + - name: Run build + run: npm run build + + deploy: + needs: build + if: github.event.pull_request.merged == true && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/server-migration') + runs-on: ubuntu-latest + steps: + - name: Trigger Deployment Webhook + run: | + curl -X POST "${{ secrets.DEPLOY_WEBHOOK_URL }}" \ + -H "Content-Type: application/json" \ + -H "x-deploy-secret: ${{ secrets.DEPLOY_SECRET }}" \ + -d '{"ref": "${{ github.ref }}", "commit": "${{ github.sha }}"}' diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 163fd3232..494be948c 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -18,7 +18,7 @@ jobs: pull-requests: write steps: - - uses: actions/stale@v5 + - uses: actions/stale@v9 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions' diff --git a/.gitignore b/.gitignore index 9314c255e..c52eec322 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,8 @@ /bazel-out # Node -/node_modules +client/node_modules +server/node_modules npm-debug.log yarn-error.log @@ -32,7 +33,8 @@ chrome-profiler-events*.json .history/* # Miscellaneous -/.angular/cache +client/.angular/cache +server/.angular/cache .sass-cache/ /connect.lock /coverage @@ -43,3 +45,18 @@ testem.log # System files .DS_Store Thumbs.db + +# SDK +client/dist +server/dist +shared/dist +node_modules +vars.ps1 +script.ps1 + +# Environment variables +server/.env + +# Logs +server/logs +*.log diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..a31971cc5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "todo-tree.tree.showBadges": false, + "chat.tools.terminal.autoApprove": { + "npm outdated": true + } +} \ No newline at end of file diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 000000000..c1fcc728a --- /dev/null +++ b/BUILD.md @@ -0,0 +1,103 @@ +# Quick Build & Deploy Reference + +## Development +```bash +npm run dev # Start dev servers (client + server + shared watch) +npm run start:client # Start Angular dev server only +npm run start:server # Start Express dev server only +``` + +## Building for Production +```bash +npm run build # Build everything to dist/ folder +npm run clean # Clean dist/ folder +npm run build:all # Build all packages without copying +npm run start:prod # Test production build locally +``` + +## What `npm run build` Does +1. **Clean**: Removes old `dist/` folder +2. **Build Shared**: Compiles TypeScript types (`shared/dist`) +3. **Build Server**: Compiles Express backend (`server/dist`) +4. **Build Client**: Compiles Angular app (`client/dist/browser`) +5. **Copy All**: Copies everything to `dist/` with production structure + +## Production Directory Structure +``` +dist/ +├── package.json # Production package.json (auto-generated) +├── .env.example # Environment template +├── README.md # Deployment instructions +├── node_modules/ # Dependencies from server/ +├── shared/ # Compiled types +├── server/ # Compiled backend +│ └── server.js # Entry point +└── client/browser/ # Angular build (served by Express) +``` + +## Deploy to Production + +### Quick Deploy +```bash +# 1. Build +npm run build + +# 2. Transfer +scp -r dist/ user@server:/var/www/f123dashboard/ + +# 3. Setup on server +ssh user@server +cd /var/www/f123dashboard +cp .env.example .env +nano .env # Add production values + +# 4. Start +pm2 start server/server.js --name f123dashboard +pm2 save +``` + +### Update Existing Deployment +```bash +# Build and transfer +npm run build +scp -r dist/ user@server:/var/www/f123dashboard/ + +# Restart +ssh user@server "pm2 restart f123dashboard" +``` + +## Environment Variables +Required in `dist/.env`: +```env +RACEFORFEDERICA_DB_DATABASE_URL=postgresql://... +JWT_SECRET=your-secret-key-min-32-chars +MAIL_USER=email@example.com +MAIL_PASS=password +RACEFORFEDERICA_DREANDOS_SECRET=twitch-secret +PORT=3000 +NODE_ENV=production +LOG_LEVEL=info +``` + +## Verify Deployment +```bash +curl http://localhost:3000/api/health +# {"status":"ok","timestamp":"...","environment":"production"} +``` + +## Common Issues + +**Server can't find client files** +- Ensure `NODE_ENV=production` is set +- Check: `ls -la dist/client/browser/index.html` + +**Database connection failed** +- Verify: `psql $RACEFORFEDERICA_DB_DATABASE_URL` + +**Port already in use** +- Change `PORT` in `.env` or stop other process + +## See Also +- [DEPLOYMENT.md](DEPLOYMENT.md) - Full deployment guide +- [server/docs/](server/docs/) - API documentation +- [.github/instructions/](\.github/instructions/) - Architecture docs diff --git a/README.md b/README.md index 7a3df98e3..22a867a5a 100644 --- a/README.md +++ b/README.md @@ -1,255 +1,158 @@ -[![@coreui angular](https://img.shields.io/badge/@coreui%20-angular-lightgrey.svg?style=flat-square)](https://github.com/coreui/angular) -[![npm-coreui-angular][npm-coreui-angular-badge]][npm-coreui-angular] -[![npm-coreui-angular][npm-coreui-angular-badge-next]][npm-coreui-angular] -[![NPM downloads][npm-coreui-angular-download]][npm-coreui-angular] -[![@coreui coreui](https://img.shields.io/badge/@coreui%20-coreui-lightgrey.svg?style=flat-square)](https://github.com/coreui/coreui) -[![npm package][npm-coreui-badge]][npm-coreui] -[![NPM downloads][npm-coreui-download]][npm-coreui] -![angular](https://img.shields.io/badge/angular-^18.2.0-lightgrey.svg?style=flat-square&logo=angular) +

+ + + +

-[npm-coreui-angular]: https://www.npmjs.com/package/@coreui/angular +# F1 Dashboard RaceForFederica -[npm-coreui-angular-badge]: https://img.shields.io/npm/v/@coreui/angular.png?style=flat-square +Full-stack dashboard for the Esport F1 23 championship (friendly, but not so much...) -[npm-coreui-angular-badge-next]: https://img.shields.io/npm/v/@coreui/angular/next?style=flat-square&color=red +**Architecture**: Angular 18+ frontend + Express.js backend + PostgreSQL database -[npm-coreui-angular-download]: https://img.shields.io/npm/dm/@coreui/angular.svg?style=flat-square +## Preview 🧐 -[npm-coreui]: https://www.npmjs.com/package/@coreui/coreui +| **Dashboard** | **Championship** | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| ![dashboard](client/src/assets/images/readme/dashboard.png) | ![championship](client/src/assets/images/readme/campionato.png) | +| **Pilots** | **Fantasy F1** | +| ![pilots](client/src/assets/images/readme/piloti.png) | ![fanta](client/src/assets/images/readme/fanta.png) | +| **Fantasy F1 Vote** | **Fantasy F1 Result** | +| ![fanta_voto](client/src/assets/images/readme/fanta_voto.png) | ![fanta_risultato](client/src/assets/images/readme/fanta_risultato.png) | -[npm-coreui-badge]: https://img.shields.io/npm/v/@coreui/coreui.png?style=flat-square +## Project Structure 📁 -[npm-coreui-download]: https://img.shields.io/npm/dm/@coreui/coreui.svg?style=flat-square - -# CoreUI Free Admin Dashboard Template for Angular 18 - -CoreUI is meant to be the UX game changer. Pure & transparent code is devoid of redundant components, so the app is light enough to offer ultimate user -experience. This means mobile devices also, where the navigation is just as easy and intuitive as on a desktop or laptop. The CoreUI Layout API lets you -customize your project for almost any device – be it Mobile, Web or WebApp – CoreUI covers them all! - -- [CoreUI Angular Admin Dashboard Template & UI Components Library](https://coreui.io/angular) -- [CoreUI Angular Demo](https://coreui.io/angular/demo/5.0/free/) -- [CoreUI Angular Docs](https://coreui.io/angular/docs/) - -## Table of Contents - -* [Versions](#versions) -* [CoreUI Pro](#coreui-pro) -* [Quick Start](#quick-start) -* [Installation](#installation) -* [Basic usage](#basic-usage) -* [What's included](#whats-included) -* [Documentation](#documentation) -* [Versioning](#versioning) -* [Creators](#creators) -* [Community](#community) -* [Copyright and License](#copyright-and-license) - -## Versions - -* [CoreUI Free Bootstrap Admin Template](https://github.com/coreui/coreui-free-bootstrap-admin-template) -* [CoreUI Free Angular Admin Template](https://github.com/coreui/coreui-free-angular-admin-template) -* [CoreUI Free React.js Admin Template](https://github.com/coreui/coreui-free-react-admin-template) -* [CoreUI Free Vue.js Admin Template](https://github.com/coreui/coreui-free-vue-admin-template) - -## CoreUI Pro - -* 💪 [CoreUI Pro Angular Admin Template](https://coreui.io/product/angular-dashboard-template/) -* 💪 [CoreUI Pro Bootstrap Admin Template](https://coreui.io/product/bootstrap-dashboard-template/) -* 💪 [CoreUI Pro React Admin Template](https://coreui.io/product/react-dashboard-template/) -* 💪 [CoreUI Pro Next.js Admin Template](https://coreui.io/product/next-js-dashboard-template/) -* 💪 [CoreUI Pro Vue Admin Template](https://coreui.io/product/vue-dashboard-template/) - -## CoreUI PRO Angular Admin Templates - -| Default Theme | Light Theme | -|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [![CoreUI PRO Angular Admin Template](https://coreui.io/images/templates/coreui_pro_default_light_dark.webp)](https://coreui.io/product/angular-dashboard-template/?theme=default) | [![CoreUI PRO Angular Admin Template](https://coreui.io/images/templates/coreui_pro_light_light_dark.webp)](https://coreui.io/product/angular-dashboard-template/?theme=light) | - -| Modern Theme | Bright Theme | -|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [![CoreUI PRO Angular Admin Template](https://coreui.io/images/templates/coreui_pro_default_v3_light_dark.webp)](https://coreui.io/product/angular-dashboard-template/?theme=default-v3) | [![CoreUI PRO React Admin Template](https://coreui.io/images/templates/coreui_pro_light_v3_light_dark.webp)](https://coreui.io/product/angular-dashboard-template/?theme=light) | - -## Quick Start - -- [Download the latest release](https://github.com/coreui/coreui-free-angular-admin-template/) -- Clone the repo: `git clone https://github.com/coreui/coreui-free-angular-admin-template.git` - -#### Prerequisites - -Before you begin, make sure your development environment includes `Node.js®` and an `npm` package manager. - -###### Node.js - -[**Angular 18**](https://angular.io/guide/what-is-angular) requires `Node.js` LTS version `^18.19` or `^20.11`. - -- To check your version, run `node -v` in a terminal/console window. -- To get `Node.js`, go to [nodejs.org](https://nodejs.org/). - -###### Angular CLI - -Install the Angular CLI globally using a terminal/console window. - -```bash -npm install -g @angular/cli ``` - -### Installation - -``` bash -$ npm install -$ npm update +f123dashboard/ +├── client/ # Angular 18+ frontend (standalone components) +├── server/ # Express.js backend with TypeScript +├── shared/ # Shared TypeScript types used by both client and server +└── docs/ # Documentation ``` -### Basic usage - -``` bash -# dev server with hot reload at http://localhost:4200 -$ npm start +## Building and Running 🛠️ + +### Prerequisites + +- Node.js (v18+) +- PostgreSQL database +- npm or yarn + +### Setup + +1. **Install dependencies** (from root): + ```bash + npm install + ``` + +2. **Configure environment variables**: + + Create `server/.env` based on `server/.env.example`: + ```env + RACEFORFEDERICA_DB_DATABASE_URL=postgresql://user:password@host:port/database + JWT_SECRET=your-secret-key + MAIL_USER=email@example.com + MAIL_PASS=email-password + RACEFORFEDERICA_DREANDOS_SECRET=twitch-secret + PORT=3000 + NODE_ENV=development + LOG_LEVEL=debug + ``` + +3. **Build shared types package**: + ```bash + cd shared + npm run build + cd .. + ``` + +### Development + +**Option 1: Run both frontend and backend concurrently** (from root): +```bash +npm run dev ``` -Navigate to [http://localhost:4200](http://localhost:4200). The app will automatically reload if you change any of the source files. - -#### Build - -Run `build` to build the project. The build artifacts will be stored in the `dist/` directory. +**Option 2: Run separately**: +Backend (from `server/`): ```bash -# build for production with minification -$ npm run build +npm run dev ``` -## What's included - -Within the download you'll find the following directories and files, logically grouping common assets and providing both compiled and minified variations. -You'll see something like this: - -``` -coreui-free-angular-admin-template -├── src/ # project root -│ ├── app/ # main app directory -| │ ├── icons/ # icons set for the app -| │ ├── layout/ # layout -| | │ └── default-layout/ # layout components -| | | └── _nav.js # sidebar navigation config -| │ └── views/ # application views -│ ├── assets/ # images, icons, etc. -│ ├── components/ # components for demo only -│ ├── scss/ # scss styles -│ └── index.html # html template -│ -├── angular.json -├── README.md -└── package.json +Frontend (from `client/`): +```bash +npm start ``` -## Documentation - -The documentation for the CoreUI Admin Template is hosted at our website [CoreUI for Angular](https://coreui.io/angular/) - ---- - -This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.0.0. - -## Versioning - -For transparency into our release cycle and in striving to maintain backward compatibility, CoreUI Free Admin Template is maintained -under [the Semantic Versioning guidelines](http://semver.org/). - -See [the Releases section of our project](https://github.com/coreui/coreui-free-angular-admin-template/releases) for changelogs for each release version. - -## Development server - -Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. - -## Code scaffolding - -Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. - -## Build - -Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. - -## Running unit tests +- Frontend runs on `http://localhost:4200` +- Backend API runs on `http://localhost:3000` +- API requests are proxied in development via `client/proxy.conf.json` -Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). +### Production Build -## Running end-to-end tests - -Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end -testing capabilities. - -## Further help - -To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. - -## Creators - -**Łukasz Holeczek** - -* -* -* - -**CoreUI team** - -* https://github.com/orgs/coreui/people - -## Community - -Get updates on CoreUI's development and chat with the project maintainers and community members. - -- Follow [@core_ui on Twitter](https://twitter.com/core_ui). -- Read and subscribe to [CoreUI Blog](https://coreui.io/blog/). - -## Support CoreUI Development - -CoreUI is an MIT-licensed open source project and is completely free to use. However, the amount of effort needed to maintain and develop new features for the -project is not sustainable without proper financial backing. You can support development by buying the [CoreUI PRO](https://coreui.io/pricing/) or by becoming a -sponsor via [Open Collective](https://opencollective.com/coreui/). +```bash +# Build all packages +npm run build - +# Start production server (serves both API and Angular app) +cd server +npm start +``` -### Platinum Sponsors +The Express server serves the Angular build from `/` and API endpoints from `/api/*`. -Support this project by [becoming a Platinum Sponsor](https://opencollective.com/coreui/contribute/platinum-sponsor-40959/). A large company logo will be added -here with a link to your website. +## Architecture 🏗️ - +### Backend (Express.js) -### Gold Sponsors +**Pattern**: Service → Controller → Route -Support this project by [becoming a Gold Sponsor](https://opencollective.com/coreui/contribute/gold-sponsor-40960/). A big company logo will be added here with -a link to your website. +- **Services** (`server/src/services/`): Business logic and database queries +- **Controllers** (`server/src/controllers/`): Request/response handlers +- **Routes** (`server/src/routes/`): API endpoint definitions +- **Middleware** (`server/src/middleware/`): Authentication (JWT), error handling +- **Config** (`server/src/config/`): Database pool, logger (Winston) - +### Frontend (Angular) -### Silver Sponsors +- **Standalone Components**: Modern Angular 18+ architecture +- **CoreUI**: UI component library +- **Services**: Use `ApiService` for all HTTP calls to Express backend +- **Shared Types**: Import from `@f123dashboard/shared` package -Support this project by [becoming a Silver Sponsor](https://opencollective.com/coreui/contribute/silver-sponsor-40967/). A medium company logo will be added -here with a link to your website. +### Shared Types Package - +- **Location**: `shared/src/models/` +- **Purpose**: Single source of truth for TypeScript types +- **Usage**: Both client and server import from `@f123dashboard/shared` +- **Build**: Run `npm run build` in `shared/` after modifying types -### Bronze Sponsors +### Authentication -Support this project by [becoming a Bronze Sponsor](https://opencollective.com/coreui/contribute/bronze-sponsor-40966/). The company avatar will show up here -with a link to your OpenCollective Profile. +- **JWT tokens** +- **Middleware chain**: `authMiddleware` → `adminMiddleware` for protected routes +- **Session management**: Tokens stored in `user_sessions` table - +### Logging -### Backers +- **Development**: Colorized console logs (level: debug) +- **Production**: JSON file logs in `server/logs/` (level: info) +- **Configuration**: Set `LOG_LEVEL` in `.env` -Thanks to all the backers and sponsors! Support this project by [becoming a backer](https://opencollective.com/coreui/contribute/backer-40965/). +### Postman Collections - +Pre-configured Postman collections are available in `server/docs/postman/`: +- `F123Dashboard.postman_collection.json` - All API endpoints +- `F123Dashboard.postman_environment.json` - Local environment +- `F123Dashboard.postman_environment.prod.json` - Production environment - +Auto-authentication flow is implemented. See `server/docs/postman/POSTMAN_README.md` for details. -## Copyright and License +## Credits 🙇 -copyright 2024 creativeLabs Łukasz Holeczek. +Credits for this small but fun project goes to: -Code released under [the MIT license](https://github.com/coreui/coreui-free-react-admin-template/blob/master/LICENSE). -There is only one limitation you can't re-distribute the CoreUI as stock. You can’t do this if you modify the CoreUI. In the past, we faced some problems with -persons who tried to sell CoreUI based templates. +- [Paolo Celada](https://github.com/paocela) +- [Federico Degioanni](https://github.com/FAST-man-33) +- [Andrea Dominici](https://github.com/DomiJAR) diff --git a/CHANGELOG.md b/client/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to client/CHANGELOG.md diff --git a/LICENSE b/client/LICENSE similarity index 100% rename from LICENSE rename to client/LICENSE diff --git a/client/README.md b/client/README.md new file mode 100644 index 000000000..7a3df98e3 --- /dev/null +++ b/client/README.md @@ -0,0 +1,255 @@ +[![@coreui angular](https://img.shields.io/badge/@coreui%20-angular-lightgrey.svg?style=flat-square)](https://github.com/coreui/angular) +[![npm-coreui-angular][npm-coreui-angular-badge]][npm-coreui-angular] +[![npm-coreui-angular][npm-coreui-angular-badge-next]][npm-coreui-angular] +[![NPM downloads][npm-coreui-angular-download]][npm-coreui-angular] +[![@coreui coreui](https://img.shields.io/badge/@coreui%20-coreui-lightgrey.svg?style=flat-square)](https://github.com/coreui/coreui) +[![npm package][npm-coreui-badge]][npm-coreui] +[![NPM downloads][npm-coreui-download]][npm-coreui] +![angular](https://img.shields.io/badge/angular-^18.2.0-lightgrey.svg?style=flat-square&logo=angular) + +[npm-coreui-angular]: https://www.npmjs.com/package/@coreui/angular + +[npm-coreui-angular-badge]: https://img.shields.io/npm/v/@coreui/angular.png?style=flat-square + +[npm-coreui-angular-badge-next]: https://img.shields.io/npm/v/@coreui/angular/next?style=flat-square&color=red + +[npm-coreui-angular-download]: https://img.shields.io/npm/dm/@coreui/angular.svg?style=flat-square + +[npm-coreui]: https://www.npmjs.com/package/@coreui/coreui + +[npm-coreui-badge]: https://img.shields.io/npm/v/@coreui/coreui.png?style=flat-square + +[npm-coreui-download]: https://img.shields.io/npm/dm/@coreui/coreui.svg?style=flat-square + +# CoreUI Free Admin Dashboard Template for Angular 18 + +CoreUI is meant to be the UX game changer. Pure & transparent code is devoid of redundant components, so the app is light enough to offer ultimate user +experience. This means mobile devices also, where the navigation is just as easy and intuitive as on a desktop or laptop. The CoreUI Layout API lets you +customize your project for almost any device – be it Mobile, Web or WebApp – CoreUI covers them all! + +- [CoreUI Angular Admin Dashboard Template & UI Components Library](https://coreui.io/angular) +- [CoreUI Angular Demo](https://coreui.io/angular/demo/5.0/free/) +- [CoreUI Angular Docs](https://coreui.io/angular/docs/) + +## Table of Contents + +* [Versions](#versions) +* [CoreUI Pro](#coreui-pro) +* [Quick Start](#quick-start) +* [Installation](#installation) +* [Basic usage](#basic-usage) +* [What's included](#whats-included) +* [Documentation](#documentation) +* [Versioning](#versioning) +* [Creators](#creators) +* [Community](#community) +* [Copyright and License](#copyright-and-license) + +## Versions + +* [CoreUI Free Bootstrap Admin Template](https://github.com/coreui/coreui-free-bootstrap-admin-template) +* [CoreUI Free Angular Admin Template](https://github.com/coreui/coreui-free-angular-admin-template) +* [CoreUI Free React.js Admin Template](https://github.com/coreui/coreui-free-react-admin-template) +* [CoreUI Free Vue.js Admin Template](https://github.com/coreui/coreui-free-vue-admin-template) + +## CoreUI Pro + +* 💪 [CoreUI Pro Angular Admin Template](https://coreui.io/product/angular-dashboard-template/) +* 💪 [CoreUI Pro Bootstrap Admin Template](https://coreui.io/product/bootstrap-dashboard-template/) +* 💪 [CoreUI Pro React Admin Template](https://coreui.io/product/react-dashboard-template/) +* 💪 [CoreUI Pro Next.js Admin Template](https://coreui.io/product/next-js-dashboard-template/) +* 💪 [CoreUI Pro Vue Admin Template](https://coreui.io/product/vue-dashboard-template/) + +## CoreUI PRO Angular Admin Templates + +| Default Theme | Light Theme | +|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [![CoreUI PRO Angular Admin Template](https://coreui.io/images/templates/coreui_pro_default_light_dark.webp)](https://coreui.io/product/angular-dashboard-template/?theme=default) | [![CoreUI PRO Angular Admin Template](https://coreui.io/images/templates/coreui_pro_light_light_dark.webp)](https://coreui.io/product/angular-dashboard-template/?theme=light) | + +| Modern Theme | Bright Theme | +|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [![CoreUI PRO Angular Admin Template](https://coreui.io/images/templates/coreui_pro_default_v3_light_dark.webp)](https://coreui.io/product/angular-dashboard-template/?theme=default-v3) | [![CoreUI PRO React Admin Template](https://coreui.io/images/templates/coreui_pro_light_v3_light_dark.webp)](https://coreui.io/product/angular-dashboard-template/?theme=light) | + +## Quick Start + +- [Download the latest release](https://github.com/coreui/coreui-free-angular-admin-template/) +- Clone the repo: `git clone https://github.com/coreui/coreui-free-angular-admin-template.git` + +#### Prerequisites + +Before you begin, make sure your development environment includes `Node.js®` and an `npm` package manager. + +###### Node.js + +[**Angular 18**](https://angular.io/guide/what-is-angular) requires `Node.js` LTS version `^18.19` or `^20.11`. + +- To check your version, run `node -v` in a terminal/console window. +- To get `Node.js`, go to [nodejs.org](https://nodejs.org/). + +###### Angular CLI + +Install the Angular CLI globally using a terminal/console window. + +```bash +npm install -g @angular/cli +``` + +### Installation + +``` bash +$ npm install +$ npm update +``` + +### Basic usage + +``` bash +# dev server with hot reload at http://localhost:4200 +$ npm start +``` + +Navigate to [http://localhost:4200](http://localhost:4200). The app will automatically reload if you change any of the source files. + +#### Build + +Run `build` to build the project. The build artifacts will be stored in the `dist/` directory. + +```bash +# build for production with minification +$ npm run build +``` + +## What's included + +Within the download you'll find the following directories and files, logically grouping common assets and providing both compiled and minified variations. +You'll see something like this: + +``` +coreui-free-angular-admin-template +├── src/ # project root +│ ├── app/ # main app directory +| │ ├── icons/ # icons set for the app +| │ ├── layout/ # layout +| | │ └── default-layout/ # layout components +| | | └── _nav.js # sidebar navigation config +| │ └── views/ # application views +│ ├── assets/ # images, icons, etc. +│ ├── components/ # components for demo only +│ ├── scss/ # scss styles +│ └── index.html # html template +│ +├── angular.json +├── README.md +└── package.json +``` + +## Documentation + +The documentation for the CoreUI Admin Template is hosted at our website [CoreUI for Angular](https://coreui.io/angular/) + +--- + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.0.0. + +## Versioning + +For transparency into our release cycle and in striving to maintain backward compatibility, CoreUI Free Admin Template is maintained +under [the Semantic Versioning guidelines](http://semver.org/). + +See [the Releases section of our project](https://github.com/coreui/coreui-free-angular-admin-template/releases) for changelogs for each release version. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end +testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. + +## Creators + +**Łukasz Holeczek** + +* +* +* + +**CoreUI team** + +* https://github.com/orgs/coreui/people + +## Community + +Get updates on CoreUI's development and chat with the project maintainers and community members. + +- Follow [@core_ui on Twitter](https://twitter.com/core_ui). +- Read and subscribe to [CoreUI Blog](https://coreui.io/blog/). + +## Support CoreUI Development + +CoreUI is an MIT-licensed open source project and is completely free to use. However, the amount of effort needed to maintain and develop new features for the +project is not sustainable without proper financial backing. You can support development by buying the [CoreUI PRO](https://coreui.io/pricing/) or by becoming a +sponsor via [Open Collective](https://opencollective.com/coreui/). + + + +### Platinum Sponsors + +Support this project by [becoming a Platinum Sponsor](https://opencollective.com/coreui/contribute/platinum-sponsor-40959/). A large company logo will be added +here with a link to your website. + + + +### Gold Sponsors + +Support this project by [becoming a Gold Sponsor](https://opencollective.com/coreui/contribute/gold-sponsor-40960/). A big company logo will be added here with +a link to your website. + + + +### Silver Sponsors + +Support this project by [becoming a Silver Sponsor](https://opencollective.com/coreui/contribute/silver-sponsor-40967/). A medium company logo will be added +here with a link to your website. + + + +### Bronze Sponsors + +Support this project by [becoming a Bronze Sponsor](https://opencollective.com/coreui/contribute/bronze-sponsor-40966/). The company avatar will show up here +with a link to your OpenCollective Profile. + + + +### Backers + +Thanks to all the backers and sponsors! Support this project by [becoming a backer](https://opencollective.com/coreui/contribute/backer-40965/). + + + + + +## Copyright and License + +copyright 2024 creativeLabs Łukasz Holeczek. + +Code released under [the MIT license](https://github.com/coreui/coreui-free-react-admin-template/blob/master/LICENSE). +There is only one limitation you can't re-distribute the CoreUI as stock. You can’t do this if you modify the CoreUI. In the past, we faced some problems with +persons who tried to sell CoreUI based templates. diff --git a/angular.json b/client/angular.json similarity index 62% rename from angular.json rename to client/angular.json index de2c431fc..edc40a0fb 100644 --- a/angular.json +++ b/client/angular.json @@ -1,7 +1,10 @@ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "cli": { - "analytics": false + "analytics": false, + "cache": { + "environment": "all" + } }, "version": 1, "newProjectRoot": "projects", @@ -21,9 +24,9 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:application", + "builder": "@angular/build:application", "options": { - "outputPath": "dist/coreui-free-angular-admin-template", + "outputPath": "dist", "index": "src/index.html", "browser": "src/main.ts", "polyfills": [ @@ -41,7 +44,12 @@ "src/scss/styles.scss" ], "scripts": [], - "allowedCommonJsDependencies": [] + "allowedCommonJsDependencies": [], + "stylePreprocessorOptions": { + "sass": { + "silenceDeprecations": [] + } + } }, "configurations": { "production": { @@ -53,23 +61,31 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "2kb", - "maximumError": "4kb" + "maximumWarning": "7mb", + "maximumError": "8mb" } ], - "outputHashing": "all" + "outputHashing": "all", + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ] }, "development": { "optimization": false, "extractLicenses": false, - "sourceMap": true, - "namedChunks": true + "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { - "builder": "@angular-devkit/build-angular:dev-server", + "builder": "@angular/build:dev-server", + "options": { + "proxyConfig": "proxy.conf.json" + }, "configurations": { "production": { "buildTarget": "coreui-free-angular-admin-template:build:production" @@ -81,17 +97,18 @@ "defaultConfiguration": "development" }, "extract-i18n": { - "builder": "@angular-devkit/build-angular:extract-i18n", + "builder": "@angular/build:extract-i18n", "options": { "buildTarget": "coreui-free-angular-admin-template:build" } }, "test": { - "builder": "@angular-devkit/build-angular:karma", + "builder": "@angular/build:karma", "options": { "polyfills": [ "zone.js", - "zone.js/testing" + "zone.js/testing", + "@angular/localize/init" ], "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", @@ -108,5 +125,31 @@ } } } + }, + "schematics": { + "@schematics/angular:component": { + "type": "component" + }, + "@schematics/angular:directive": { + "type": "directive" + }, + "@schematics/angular:service": { + "type": "service" + }, + "@schematics/angular:guard": { + "typeSeparator": "." + }, + "@schematics/angular:interceptor": { + "typeSeparator": "." + }, + "@schematics/angular:module": { + "typeSeparator": "." + }, + "@schematics/angular:pipe": { + "typeSeparator": "." + }, + "@schematics/angular:resolver": { + "typeSeparator": "." + } } } diff --git a/client/eslint.config.js b/client/eslint.config.js new file mode 100644 index 000000000..656c99bc5 --- /dev/null +++ b/client/eslint.config.js @@ -0,0 +1,136 @@ +// @ts-check +const eslint = require("@eslint/js"); +const tseslint = require("typescript-eslint"); +const angular = require("angular-eslint"); + +module.exports = [ + { + ignores: [ + "node_modules/**", + "dist/**", + ".angular/**", + "coverage/**", + "**/*.js.map", + "**/*.d.ts", + ".eslintcache", + ], + }, + // Base ESLint and TypeScript configs for .ts files + { + files: ["**/*.ts"], + ...eslint.configs.recommended, + }, + ...tseslint.configs.recommended.map(config => ({ + ...config, + files: ["**/*.ts"], + })), + ...tseslint.configs.stylistic.map(config => ({ + ...config, + files: ["**/*.ts"], + })), + ...angular.configs.tsRecommended.map(config => ({ + ...config, + files: ["**/*.ts"], + })), + { + files: ["**/*.ts"], + processor: angular.processInlineTemplates, + rules: { + "@angular-eslint/directive-selector": [ + "error", + { + type: "attribute", + prefix: "app", + style: "camelCase", + }, + ], + "@angular-eslint/component-selector": [ + "error", + { + type: "element", + prefix: "app", + style: "kebab-case", + }, + ], + "@angular-eslint/no-empty-lifecycle-method": "warn", + "@angular-eslint/no-output-native": "error", + "@angular-eslint/no-output-on-prefix": "error", + "@angular-eslint/use-lifecycle-interface": "error", + "@angular-eslint/prefer-on-push-component-change-detection": "off", + + // TypeScript-specific rules + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/naming-convention": [ + "error", + { + selector: "default", + format: ["camelCase"], + leadingUnderscore: "allow", + trailingUnderscore: "allow", + }, + { + selector: "variable", + format: ["camelCase", "UPPER_CASE"], + leadingUnderscore: "allow", + trailingUnderscore: "allow", + }, + { + selector: "typeLike", + format: ["PascalCase"], + }, + { + selector: "enumMember", + format: ["PascalCase", "UPPER_CASE"], + }, + { + selector: "property", + format: null, // Allow any format for properties (e.g., API responses) + }, + ], + + // General code quality + "no-console": ["warn", { allow: ["warn", "error"] }], + "no-debugger": "error", + "prefer-const": "error", + "no-var": "error", + "eqeqeq": ["error", "always"], + "curly": "error", + "no-throw-literal": "error", + }, + }, + // Template configs for .html files + ...angular.configs.templateRecommended.map(config => ({ + ...config, + files: ["**/*.html"], + })), + ...angular.configs.templateAccessibility.map(config => ({ + ...config, + files: ["**/*.html"], + })), + { + files: ["**/*.html"], + rules: { + "@angular-eslint/template/click-events-have-key-events": "warn", + "@angular-eslint/template/interactive-supports-focus": "warn", + "@angular-eslint/template/no-autofocus": "warn", + "@angular-eslint/template/label-has-associated-control": "warn", + }, + }, + // Spec file overrides + { + files: ["**/*.spec.ts"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": "off", + }, + }, +]; diff --git a/karma.conf.js b/client/karma.conf.js similarity index 94% rename from karma.conf.js rename to client/karma.conf.js index 2a87249b6..e34b90353 100644 --- a/karma.conf.js +++ b/client/karma.conf.js @@ -9,8 +9,7 @@ module.exports = function(config) { require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), - require('karma-coverage'), - require('@angular-devkit/build-angular/plugins/karma') + require('karma-coverage') ], client: { jasmine: { diff --git a/client/package.json b/client/package.json new file mode 100644 index 000000000..8e97e9f7d --- /dev/null +++ b/client/package.json @@ -0,0 +1,81 @@ +{ + "name": "race-for-federica-dashboard", + "version": "2.0.0", + "license": "MIT", + "author": "Race for federica Team", + "homepage": "https://f123dashboard.app.genez.io", + "config": { + "theme": "default", + "coreui_library_short_version": "5.2", + "coreui_library_docs_url": "https://coreui.io/angular/docs/" + }, + "scripts": { + "ng": "ng", + "start": "ng serve -o --host 0.0.0.0", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test", + "lint": "eslint \"src/**/*.{ts,html}\"", + "lint:fix": "eslint \"src/**/*.{ts,html}\" --fix" + }, + "private": true, + "dependencies": { + "@angular/animations": "^21.0.6", + "@angular/cdk": "^21.0.5", + "@angular/common": "^21.0.6", + "@angular/compiler": "^21.0.6", + "@angular/core": "^21.0.6", + "@angular/forms": "^21.0.6", + "@angular/language-service": "^21.0.6", + "@angular/localize": "^21.0.6", + "@angular/material": "^21.0.0", + "@angular/platform-browser": "^21.0.6", + "@angular/platform-browser-dynamic": "^21.0.6", + "@angular/router": "^21.0.6", + "@coreui/angular": "~5.6.4", + "@coreui/angular-chartjs": "~5.6.4", + "@coreui/chartjs": "^4.2.0", + "@coreui/coreui": "~5.5.0", + "@coreui/icons": "^3.0.1", + "@coreui/icons-angular": "~5.6.4", + "@coreui/utils": "^2.0.2", + "@f123dashboard/shared": "file:../shared", + "@ng-bootstrap/ng-bootstrap": "^20.0.0", + "angularx-flatpickr": "^8.0.0", + "chart.js": "^4.5.1", + "flag-icons": "^7.2.3", + "flatpickr": "^4.6.13", + "lodash-es": "^4.17.22", + "ngx-scrollbar": "^19.1.4", + "rxjs": "~7.8.2", + "tslib": "^2.8.1", + "zone.js": "~0.16.0" + }, + "devDependencies": { + "@angular/build": "^21.0.4", + "@angular/cli": "^21.0.4", + "@angular/compiler-cli": "^21.0.6", + "@eslint/js": "^9.18.0", + "@types/jasmine": "^6.0.0", + "@types/lodash-es": "^4.17.12", + "@types/node": "^25.2.1", + "angular-eslint": "^21.2.0", + "eslint": "^9.18.0", + "jasmine-core": "^6.0.1", + "karma": "^6.4.4", + "karma-chrome-launcher": "^3.2.0", + "karma-coverage": "^2.2.1", + "karma-jasmine": "^5.1.0", + "karma-jasmine-html-reporter": "^2.1.0", + "typescript": "~5.9.0", + "typescript-eslint": "^8.19.1" + }, + "overrides": { + "parse5": "8.0.0", + "sass": "^1.97.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || ^24.0.0", + "npm": ">=10" + } +} diff --git a/client/proxy.conf.json b/client/proxy.conf.json new file mode 100644 index 000000000..9ed18d7de --- /dev/null +++ b/client/proxy.conf.json @@ -0,0 +1,8 @@ +{ + "/api": { + "target": "http://localhost:3000", + "secure": false, + "logLevel": "debug", + "changeOrigin": true + } +} diff --git a/src/app/app.component.spec.ts b/client/src/app/app.component.spec.ts similarity index 67% rename from src/app/app.component.spec.ts rename to client/src/app/app.component.spec.ts index 4e1e5a832..a52eb734f 100644 --- a/src/app/app.component.spec.ts +++ b/client/src/app/app.component.spec.ts @@ -1,14 +1,14 @@ import { TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { provideRouter } from '@angular/router'; import { AppComponent } from './app.component'; describe('AppComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ - AppComponent + imports: [AppComponent ], - providers: [provideRouter([])] + providers: [provideNoopAnimations(), provideRouter([])] }).compileComponents(); }); @@ -18,9 +18,9 @@ describe('AppComponent', () => { expect(app).toBeTruthy(); }); - it(`should have as title 'CoreUI Angular Admin Template'`, () => { + it(`should have as title 'F1 RaceForFederica'`, () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; - expect(app.title).toEqual('CoreUI Angular Admin Template'); + expect(app.title).toEqual('F1 RaceForFederica'); }); }); diff --git a/src/app/app.component.ts b/client/src/app/app.component.ts similarity index 60% rename from src/app/app.component.ts rename to client/src/app/app.component.ts index 3fa624d05..f8165246c 100644 --- a/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { Component, DestroyRef, inject, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, DestroyRef, OnInit, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Title } from '@angular/platform-browser'; import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router'; @@ -6,16 +6,26 @@ import { delay, filter, map, tap } from 'rxjs/operators'; import { ColorModeService } from '@coreui/angular'; import { IconSetService } from '@coreui/icons-angular'; +import { cilCoffee } from '@coreui/icons'; + import { iconSubset } from './icons/icon-subset'; +import { DbDataService } from 'src/app/service/db-data.service'; +import { SeasonService } from 'src/app/service/season.service'; +import { TwitchApiService } from './service/twitch-api.service'; @Component({ selector: 'app-root', - template: '', standalone: true, + template: '', imports: [RouterOutlet] }) export class AppComponent implements OnInit { - title = 'CoreUI Angular Admin Template'; + private dbData = inject(DbDataService); + private cdr = inject(ChangeDetectorRef); + private seasonsService = inject(SeasonService); + private twitchApiService = inject(TwitchApiService); + + title = 'F1 RaceForFederica'; readonly #destroyRef: DestroyRef = inject(DestroyRef); readonly #activatedRoute: ActivatedRoute = inject(ActivatedRoute); @@ -27,17 +37,15 @@ export class AppComponent implements OnInit { constructor() { this.#titleService.setTitle(this.title); - // iconSet singleton - this.#iconSetService.icons = { ...iconSubset }; + this.#iconSetService.icons = { ...iconSubset, cilCoffee }; this.#colorModeService.localStorageItemName.set('coreui-free-angular-admin-template-theme-default'); this.#colorModeService.eventName.set('ColorSchemeChange'); } - ngOnInit(): void { + piloti: any[] = []; - this.#router.events.pipe( - takeUntilDestroyed(this.#destroyRef) - ).subscribe((evt) => { + ngOnInit(): void { + this.#router.events.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe((evt) => { if (!(evt instanceof NavigationEnd)) { return; } @@ -46,13 +54,15 @@ export class AppComponent implements OnInit { this.#activatedRoute.queryParams .pipe( delay(1), - map(params => params['theme']?.match(/^[A-Za-z0-9\s]+/)?.[0]), - filter(theme => ['dark', 'light', 'auto'].includes(theme)), - tap(theme => { + map((params) => params['theme']?.match(/^[A-Za-z0-9\s]+/)?.[0] as string), + filter((theme) => ['dark', 'light', 'auto'].includes(theme)), + tap((theme) => { this.#colorModeService.colorMode.set(theme); }), takeUntilDestroyed(this.#destroyRef) ) .subscribe(); + + this.cdr.detectChanges(); } } diff --git a/client/src/app/app.config.ts b/client/src/app/app.config.ts new file mode 100644 index 000000000..5b5bc5268 --- /dev/null +++ b/client/src/app/app.config.ts @@ -0,0 +1,64 @@ +import { ApplicationConfig, importProvidersFrom, inject, provideAppInitializer } from '@angular/core'; +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; +import { provideHttpClient } from '@angular/common/http'; +import { DbDataService } from './service/db-data.service'; +import { FantaService } from './service/fanta.service'; +import { + provideRouter, + withEnabledBlockingInitialNavigation, + withHashLocation, + withInMemoryScrolling, + withRouterConfig, + withViewTransitions +} from '@angular/router'; + +import { DropdownModule, SidebarModule } from '@coreui/angular'; +import { IconSetService } from '@coreui/icons-angular'; +import { routes } from './app.routes'; +import { TwitchApiService } from './service/twitch-api.service'; +import { PlaygroundService } from './service/playground.service'; +import { registerLocaleData } from '@angular/common'; +import localeIt from '@angular/common/locales/it'; + +registerLocaleData(localeIt, 'it-IT'); + +export function initializeApp(dbDataService: DbDataService, twitchApiService: TwitchApiService, playgroundService: PlaygroundService, fantaService: FantaService) { + return async () => { + await Promise.all([ + dbDataService.allData(), + playgroundService.allData(), + fantaService.loadFantaVotes(), + twitchApiService.checkStreamStatus().catch(err => { + console.error('Error during Twitch stream status check:', err); + }) + ]); + }; +} + +export const appConfig: ApplicationConfig = { + providers: [ + provideRouter(routes, + withRouterConfig({ + onSameUrlNavigation: 'reload' + }), + withInMemoryScrolling({ + scrollPositionRestoration: 'top', + anchorScrolling: 'enabled' + }), + withEnabledBlockingInitialNavigation(), + withViewTransitions(), + withHashLocation() + ), + importProvidersFrom(SidebarModule, DropdownModule), + IconSetService, + provideAnimationsAsync(), + + provideHttpClient(), + DbDataService, + TwitchApiService, + provideAppInitializer(() => { + const initializerFn = (initializeApp)(inject(DbDataService), inject(TwitchApiService), inject(PlaygroundService), inject(FantaService)); + return initializerFn(); + }) + ] +}; diff --git a/client/src/app/app.routes.ts b/client/src/app/app.routes.ts new file mode 100644 index 000000000..a8f76c0b8 --- /dev/null +++ b/client/src/app/app.routes.ts @@ -0,0 +1,74 @@ +import { Routes } from '@angular/router'; +import { DefaultLayoutComponent } from './layout'; +import { authGuard } from './guard/auth.guard'; +import { adminGuard } from './guard/admin.guard'; + +export const routes: Routes = [ + { + path: '', + redirectTo: 'dashboard', + pathMatch: 'full' + }, + { + path: '', + component: DefaultLayoutComponent, + data: { + title: 'Home' + }, + children: [ + { + path: 'dashboard', + loadChildren: () => import('./views/dashboard/routes').then((m) => m.routes) + }, + + /* routing piloti */ + { + path: 'piloti', + loadChildren: () => import('./views/piloti/routes').then((m) => m.routes) + }, + /* routing regole */ + { + path: 'regole', + loadChildren: () => import('./views/regole/routes').then((m) => m.routes) + }, + /* routing calendario */ + { + path: 'championship', + loadChildren: () => import('./views/championship/routes').then((m) => m.routes) + }, + /* routing fanta-dashboard */ + { + path: 'fanta-dashboard', + loadChildren: () => import('./views/fanta-dashboard/routes').then((m) => m.routes), + }, + /* routing fanta */ + { + path: 'fanta', + loadChildren: () => import('./views/fanta/routes').then((m) => m.routes), + canActivate: [authGuard] + }, + /* routing admin */ + { + path: 'admin', + loadChildren: () => import('./views/admin/routes').then((m) => m.routes), + canActivate: [adminGuard] + }, + /* routing credits */ + { + path: 'credits', + loadChildren: () => import('./views/credits/routes').then((m) => m.routes) + }, + /* routing albo d'oro */ + { + path: 'albo-d-oro', + loadChildren: () => import('./views/albo-d-oro/routes').then((m) => m.routes) + }, + /* routing playground */ + { + path: 'playground', + loadChildren: () => import('./views/playground/routes').then((m) => m.routes) + } + ] + }, + { path: '**', redirectTo: 'dashboard' } +]; \ No newline at end of file diff --git a/client/src/app/components/calendar/calendar.component.html b/client/src/app/components/calendar/calendar.component.html new file mode 100644 index 000000000..2a6667d20 --- /dev/null +++ b/client/src/app/components/calendar/calendar.component.html @@ -0,0 +1,254 @@ +
+
+
+
+
{{ title() }}
+
{{ currentLabel() }}
+
+ +
+ + + + + + + + + + + +
+
+
+ +
+ + @if (view() === 'week') { + + @if (!compactView()) { +
+ + + + + @for (day of weekDays(); track day.date) { + + } + + + + @for (hour of hours; track hour) { + + + @for (day of weekDays(); track day.date) { + + } + + } + +
Orario +
{{ day.label.split(' ')[0] }}
+
{{ day.label.split(' ')[1] }}
+
+ {{ hour < 10 ? '0' + hour : hour }}:00 + + + @for (event of getEventsForSlot(day.date, hour); track $index) { +
+
{{ event.name }}
+ @if (event.description) { +
+ {{ event.description }} +
+ } +
+ } + +
+
+ } + + +
+
+ @for (day of weekDays(); track day.date) { +
+
+
+ {{ day.label }} +
+
+ +
+ + @let dayEvents = getEventsForSlot(day.date); + + @if (dayEvents.length > 0) { + @for (event of dayEvents; track $index) { +
+
+ {{ event.parsedDate | date:'HH:mm' }} +
{{ event.name }}
+
+ @if (event.description) { +
{{ event.description }}
+ } +
+ } + } @else { +
Nessun evento pianificato
+ } +
+
+ } +
+
+ } + + + @if (view() === 'list') { +
+ @if (visibleListDays().length === 0) { +
+ Nessun evento trovato. +
+ } + + @for (day of visibleListDays(); track day.date) { +
+
+
+ {{ day.label }} +
+
+ +
+ @let dayEvents = getEventsForSlot(day.date); + + @for (event of dayEvents; track $index) { +
+
+ {{ event.parsedDate | date:'HH:mm' }} +
{{ event.name }}
+
+ @if (event.description) { +
{{ event.description }}
+ } +
+ } +
+
+ } +
+ } + + + @if (view() === 'month') { +
+ + + + @for (dayName of weekHeaders(); track dayName) { + + } + + + + @for (week of monthgrid(); track $index) { + + @for (day of week; track day.date) { + + } + + } + +
{{ dayName.slice(0, 3) }}{{ dayName.slice(3) }}
+ +
+ {{ day.dayNumber }} +
+ + +
+ @for (event of getEventsForSlot(day.date); track $index) { +
+ {{ event.name }} + {{ event.name }} +
+ } +
+ + + +
+
+ } +
+
+ + + +
{{ selectedEvent()?.name }}
+ +
+ + @if (selectedEvent()) { +
+ Data: {{ selectedEvent()!.parsedDate | date:'fullDate':'':'it-IT' }} +
+
+ Orario: {{ selectedEvent()!.parsedDate | date:'HH:mm' }} +
+ @if (selectedEvent()?.description) { +
+ Descrizione: +

{{ selectedEvent()!.description }}

+
+ } + } +
+ + + +
diff --git a/client/src/app/components/calendar/calendar.component.scss b/client/src/app/components/calendar/calendar.component.scss new file mode 100644 index 000000000..1c1da4711 --- /dev/null +++ b/client/src/app/components/calendar/calendar.component.scss @@ -0,0 +1,157 @@ +:host { + display: block; +} + +// Week View Table (Desktop) +.week-view-table { + min-width: 800px; + table-layout: fixed; + + th, + td { + border-color: var(--cui-border-color, #dee2e6); + } + + .time-column { + width: 70px; + position: sticky; + left: 0; + z-index: 10; + // Ensure background matches body background for sticky behavior + background-color: var(--cui-body-bg, #fff); + border-right: 2px solid var(--cui-border-color, #dee2e6); + } + + .sticky-header { + // Ensure header background overrides potential transparency + background-color: var(--cui-body-bg, #fff); + th { + background-color: var(--cui-body-bg, #fff); // Explicitly set TH background + } + } +} + +thead.sticky-top { + z-index: 20; + th { + box-shadow: 0 1px 0 var(--cui-border-color, #dee2e6); + } + th.time-column { + z-index: 30; + } +} + +.today-highlight { + background-color: rgba(var(--cui-primary-rgb, 50, 31, 219), 0.15) !important; + position: relative; + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: 1px solid var(--cui-primary, #321fdb); + pointer-events: none; + } +} + +.outside-month { + background-color: var(--cui-secondary-bg, #e9ecef) !important; + opacity: 0.7; +} + +.event-slot { + min-height: 80px; // Allow growth for content + transition: background-color 0.2s; + &:hover { + background-color: rgba(var(--cui-primary-rgb, 50, 31, 219), 0.03); + } +} + +.event-card { + cursor: pointer; + border-left-width: 3px !important; + font-size: 0.8rem; + line-height: 1.2; + transition: transform 0.1s ease-in-out; + &:hover { + transform: scale(1.02); + z-index: 5; + } +} + +.mobile-event-card { + cursor: pointer; + transition: transform 0.1s ease-in-out, background-color 0.2s; + + &:hover, &:focus { + background-color: var(--cui-light-hover, #d8dbe0) !important; + transform: translateX(2px); + } +} + +.month-event-item { + cursor: pointer; + transition: opacity 0.2s; + &:hover { + opacity: 0.8; + } +} + +// Month View Table +.month-view-table { + table-layout: fixed; + + th.month-header-cell { + font-size: 0.9rem; + overflow: hidden; + text-overflow: ellipsis; + padding: 0.5rem 0.25rem; + } + + td.month-cell { + height: 100px; // Default height for desktop + + @media (max-width: 768px) { + height: 70px; + } + } + + .day-number { + font-size: 0.9rem; + @media (max-width: 768px) { + font-size: 0.8rem; + } + } +} + +// Mobile List View overrides +.list-group-item { + border-left: none; + border-right: none; + + &:first-child { + border-top: none; + } +} + +// Dark mode overrides +@media (prefers-color-scheme: dark) { + .week-view-table .time-column, + .week-view-table .sticky-header th { + background-color: var(--cui-body-bg, #212529) !important; + } +} + +:host-context([data-coreui-theme="dark"]) { + .week-view-table .time-column, + .week-view-table .sticky-header th { + background-color: var(--cui-body-bg, #212529) !important; + } + + .outside-month { + background-color: rgba(0, 0, 0, 0.2) !important; + opacity: 0.5; + } +} diff --git a/client/src/app/components/calendar/calendar.component.spec.ts b/client/src/app/components/calendar/calendar.component.spec.ts new file mode 100644 index 000000000..d23153983 --- /dev/null +++ b/client/src/app/components/calendar/calendar.component.spec.ts @@ -0,0 +1,103 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { CalendarComponent } from './calendar.component'; +import { DatePipe } from '@angular/common'; + +describe('CalendarComponent', () => { + let component: CalendarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CalendarComponent], + providers: [provideNoopAnimations(), DatePipe] + }).compileComponents(); + + fixture = TestBed.createComponent(CalendarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should default to week view', () => { + expect(component.view()).toBe('week'); + }); + + it('should switch views', () => { + component.setView('month'); + expect(component.view()).toBe('month'); + component.setView('week'); + expect(component.view()).toBe('week'); + }); + + it('should take input for default view', () => { + fixture.componentRef.setInput('defaultView', 'month'); + fixture.detectChanges(); + expect(component.view()).toBe('month'); + }); + + it('should navigate dates correctly in week view', () => { + const initial = new Date(component.currentDate()); + + // Next Week + component.next(); + let current = component.currentDate(); + // Use toBeCloseTo with precision 0 to tolerate a DST-related 1-hour offset + const diffDays = (current.getTime() - initial.getTime()) / (1000 * 3600 * 24); + expect(diffDays).toBeCloseTo(7, 0); + + // Prev Week (back to start) + component.prev(); + current = component.currentDate(); + expect(current.getFullYear()).toBe(initial.getFullYear()); + expect(current.getMonth()).toBe(initial.getMonth()); + expect(current.getDate()).toBe(initial.getDate()); + }); + + it('should navigate dates correctly in month view', () => { + component.setView('month'); + const initial = new Date(component.currentDate()); + + // Next Month + component.next(); + const current = component.currentDate(); + // Rough check: month should be different + expect(current.getMonth()).not.toBe(initial.getMonth()); + + // Check specific month diff logic (approx 28-31 days) + // Or check getMonth() increased by 1 (wrapping around year) + const expectedMonth = (initial.getMonth() + 1) % 12; + expect(current.getMonth()).toBe(expectedMonth); + }); + + it('should process events correctly', () => { + // Recreate the component with events already set before initialization + const testDate = new Date(); + testDate.setHours(10, 0, 0, 0); + + const newFixture = TestBed.createComponent(CalendarComponent); + const newComponent = newFixture.componentInstance; + + // Set events before change detection runs + newFixture.componentRef.setInput('events', [ + { name: 'Test Event', date: testDate, description: 'Desc' } + ]); + + // Now run change detection + newFixture.detectChanges(); + + const events = newComponent.processedEvents(); + expect(events.length).toBe(1); + expect(events[0].name).toBe('Test Event'); + + // Check slot mapping + const slotEvents = newComponent.getEventsForSlot(testDate, 10); + expect(slotEvents.length).toBe(1); + + const wrongSlotEvents = newComponent.getEventsForSlot(testDate, 11); + expect(wrongSlotEvents.length).toBe(0); + }); +}); diff --git a/client/src/app/components/calendar/calendar.component.ts b/client/src/app/components/calendar/calendar.component.ts new file mode 100644 index 000000000..53ab9288c --- /dev/null +++ b/client/src/app/components/calendar/calendar.component.ts @@ -0,0 +1,361 @@ +import { Component, input, computed, signal, ChangeDetectionStrategy, effect } from '@angular/core'; +import { CommonModule, DatePipe } from '@angular/common'; +import { ButtonModule, ButtonGroupModule, ModalModule } from '@coreui/angular'; +import { IconModule } from '@coreui/icons-angular'; + +export interface CalendarEvent { + name: string; + date: Date | string; // Support both object and ISO string + description?: string; + color?: string; // Optional color for the event +} + +export interface ProcessedCalendarEvent extends CalendarEvent { + parsedDate: Date; +} + +@Component({ + selector: 'app-calendar', + imports: [CommonModule, ButtonModule, ButtonGroupModule, IconModule, ModalModule], + providers: [DatePipe], + templateUrl: './calendar.component.html', + styleUrls: ['./calendar.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +/** + * A calendar component that displays events in week or month view. + * Supports navigation, event selection, and modal display. + */ +export class CalendarComponent { + /** + * The title displayed at the top of the calendar. + * @default 'Calendar' + */ + title = input('Calendar'); + + /** + * The default view mode for the calendar. + * Can be either 'week', 'month', or 'list'. + * @default 'week' + */ + defaultView = input<'week' | 'month' | 'list'>('week'); + + /** + * An array of calendar events to be displayed. + * Each event should conform to the CalendarEvent interface. + * @default [] + */ + events = input([]); + + /** + * If true, forces the mobile list view on all screen sizes for the week view. + * @default false + */ + compactView = input(false); + + + + readonly view = signal<'week' | 'month' | 'list'>('week'); + readonly currentDate = signal(new Date()); + readonly listPage = signal(0); + readonly hours = Array.from({ length: 24 }, (_, i) => i); + // Modal state + readonly selectedEvent = signal(null); + readonly eventModalVisible = signal(false); + + // Locale for formatting (Italian as requested in examples) + private readonly locale = 'it-IT'; + + constructor() { + // Sync view with defaultView input using effect + effect(() => { + const newDefaultView = this.defaultView(); + this.view.set(newDefaultView); + + // If switching to list view, calculate the appropriate page + if (newDefaultView === 'list') { + this.calculateListPageForDate(this.currentDate()); + } + }); + + // Watch events changes - if events are loaded and in list view, sync to today + effect(() => { + const currentEvents = this.events(); + if (this.view() === 'list' && currentEvents.length > 0) { + this.calculateListPageForDate(this.currentDate()); + } + }); + } + + + readonly currentLabel = computed(() => { + const date = this.currentDate(); + const view = this.view(); + + if (view === 'month') { + // e.g., Set 2026 + return date.toLocaleDateString(this.locale, { month: 'long', year: 'numeric' }); + } else if (view === 'week') { + // Week View: Show range or current month + // e.g., 26 Gen - 01 Feb 2026 + const start = this.getStartOfWeek(date); + const end = new Date(start); + end.setDate(end.getDate() + 6); + + const startStr = start.toLocaleDateString(this.locale, { day: 'numeric', month: 'short' }); + const endStr = end.toLocaleDateString(this.locale, { day: 'numeric', month: 'short', year: 'numeric' }); + return `${startStr} - ${endStr}`; + } else { + // List View: Show range of events on current page + const visibleDays = this.visibleListDays(); + if (visibleDays.length === 0) { + return 'Nessun Evento'; + } + + const start = visibleDays[0].date; + const end = visibleDays[visibleDays.length - 1].date; + + const startStr = start.toLocaleDateString(this.locale, { day: 'numeric', month: 'short' }); + const endStr = end.toLocaleDateString(this.locale, { day: 'numeric', month: 'short', year: 'numeric' }); + return `Eventi: ${startStr} - ${endStr}`; + } + }); + + readonly sortedUniqueEventDates = computed(() => { + const events = this.processedEvents(); + const dates = new Set(); + events.forEach(e => { + const d = new Date(e.parsedDate); + d.setHours(0, 0, 0, 0); + dates.add(d.getTime()); + }); + return Array.from(dates).sort((a, b) => a - b).map(t => new Date(t)); + }); + + readonly visibleListDays = computed(() => { + const allDates = this.sortedUniqueEventDates(); + const page = this.listPage(); + const pageSize = 7; + + // Ensure page valid + const startIndex = page * pageSize; + + return allDates.slice(startIndex, startIndex + pageSize).map(d => ({ + date: d, + label: d.toLocaleDateString(this.locale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }), + isToday: this.isSameDay(d, new Date()) + })); + }); + + readonly weekDays = computed(() => { + const start = this.getStartOfWeek(this.currentDate()); + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(start); + d.setDate(d.getDate() + i); + return { + date: d, + label: d.toLocaleDateString(this.locale, { weekday: 'long', day: 'numeric' }), // Lunedì 11 + isToday: this.isSameDay(d, new Date()) + }; + }); + }); + + readonly weekHeaders = computed(() => { + // Generate headers Lun...Dom for month view + // Use a fixed date that is a Monday to start (e.g., Jan 1 2024 is a Monday) + const baseDate = new Date(2024, 0, 1); + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(baseDate); + d.setDate(baseDate.getDate() + i); + return d.toLocaleDateString(this.locale, { weekday: 'long' }); + }); + }); + + readonly monthgrid = computed(() => { + const date = this.currentDate(); + const year = date.getFullYear(); + const month = date.getMonth(); + + const firstDayOfMonth = new Date(year, month, 1); + const startDate = this.getStartOfWeek(firstDayOfMonth); + + const weeks = []; + const currentDay = new Date(startDate); + + // Generate 6 weeks to cover all possible month overlaps (max 42 days) + for (let w = 0; w < 6; w++) { + const week = []; + for (let d = 0; d < 7; d++) { + week.push({ + date: new Date(currentDay), + dayNumber: currentDay.getDate(), + isCurrentMonth: currentDay.getMonth() === month, + isToday: this.isSameDay(currentDay, new Date()) + }); + currentDay.setDate(currentDay.getDate() + 1); + } + weeks.push(week); + + // Stop if we've moved to the next month and completed the week + if (currentDay.getMonth() !== month && w >= 4) { + break; + } + } + return weeks; + }); + + readonly processedEvents = computed(() => { + // Map events to easy lookup structure + return this.events().map(e => ({ + ...e, + parsedDate: this.parseAsLocalTime(e.date) + })); + }); + + // --- Navigation Methods --- + + setView(view: 'week' | 'month' | 'list') { + this.view.set(view); + if (view === 'list') { + this.calculateListPageForDate(this.currentDate()); + } + } + + next() { + if (this.view() === 'list') { + const allDates = this.sortedUniqueEventDates(); + const maxPage = Math.ceil(allDates.length / 7) - 1; + if (this.listPage() < maxPage) { + this.listPage.update(p => p + 1); + } + return; + } + + const date = new Date(this.currentDate()); + if (this.view() === 'week') { + date.setDate(date.getDate() + 7); + } else { + date.setMonth(date.getMonth() + 1); + } + this.currentDate.set(date); + } + + prev() { + if (this.view() === 'list') { + if (this.listPage() > 0) { + this.listPage.update(p => p - 1); + } + return; + } + + const date = new Date(this.currentDate()); + if (this.view() === 'week') { + date.setDate(date.getDate() - 7); + } else { + date.setMonth(date.getMonth() - 1); + } + this.currentDate.set(date); + } + + today() { + this.currentDate.set(new Date()); + if (this.view() === 'list') { + this.calculateListPageForDate(new Date()); + } + } + + private calculateListPageForDate(date: Date) { + const allDates = this.sortedUniqueEventDates(); + if (allDates.length === 0) { + this.listPage.set(0); + return; + } + + // Find the first date >= given date + const targetTime = new Date(date); + targetTime.setHours(0,0,0,0); + const targetTs = targetTime.getTime(); + + let index = allDates.findIndex(d => d.getTime() >= targetTs); + + // If not found (all events are in past), show last page? Or closest? + // Let's default to closest (last one if all past) + if (index === -1) { + index = allDates.length - 1; + // If empty handled above + } + + this.listPage.set(Math.floor(index / 7)); + } + + // --- Modal Methods --- + + handleEventClick(event: ProcessedCalendarEvent) { + this.selectedEvent.set(event); + this.eventModalVisible.set(true); + } + + handleModalChange(visible: boolean) { + this.eventModalVisible.set(visible); + if (!visible) { + // Optional: delay clearing to avoid content flash during animation + setTimeout(() => this.selectedEvent.set(null), 300); + } + } + + // --- Helper Methods --- + + /** + * Parse a date/string as local time, ignoring timezone indicators. + * This prevents ISO strings with 'Z' from being converted to local timezone. + */ + private parseAsLocalTime(date: Date | string): Date { + if (date instanceof Date) { + return date; + } + + // Parse ISO string components and create local date + const match = date.match(/^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?)?$/); + if (match) { + const [, year, month, day, hour = '0', minute = '0', second = '0'] = match; + return new Date( + parseInt(year), + parseInt(month) - 1, + parseInt(day), + parseInt(hour), + parseInt(minute), + parseInt(second) + ); + } + + // Fallback to standard parsing + return new Date(date); + } + + private getStartOfWeek(date: Date): Date { + const d = new Date(date); + const day = d.getDay(); + const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Adjust when day is Sunday + d.setDate(diff); + d.setHours(0, 0, 0, 0); + return d; + } + + private isSameDay(d1: Date, d2: Date): boolean { + return d1.getFullYear() === d2.getFullYear() && + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate(); + } + + getEventsForSlot(date: Date, hour?: number): ProcessedCalendarEvent[] { + return this.processedEvents().filter(e => { + const isSameDay = this.isSameDay(e.parsedDate, date); + if (!isSameDay) {return false;} + + if (hour !== undefined) { + return e.parsedDate.getHours() === hour; + } + return true; + }); + } +} diff --git a/client/src/app/components/championship-trend/championship-trend.component.html b/client/src/app/components/championship-trend/championship-trend.component.html new file mode 100644 index 000000000..4d3f911a3 --- /dev/null +++ b/client/src/app/components/championship-trend/championship-trend.component.html @@ -0,0 +1,90 @@ + + + + +

Andamento Campionato 📈

+
+ Trend dei piloti per ogni GP +
+
+ +
+ + + + + + + + +
+
+
+ + Main chart + +
+ + + + +
\ No newline at end of file diff --git a/src/app/layout/default-layout/default-footer/default-footer.component.scss b/client/src/app/components/championship-trend/championship-trend.component.scss similarity index 100% rename from src/app/layout/default-layout/default-footer/default-footer.component.scss rename to client/src/app/components/championship-trend/championship-trend.component.scss diff --git a/client/src/app/components/championship-trend/championship-trend.component.spec.ts b/client/src/app/components/championship-trend/championship-trend.component.spec.ts new file mode 100644 index 000000000..3b4e6bc98 --- /dev/null +++ b/client/src/app/components/championship-trend/championship-trend.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; + +import { ChampionshipTrendComponent } from './championship-trend.component'; + +describe('ChampionshipTrendComponent', () => { + let component: ChampionshipTrendComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [provideNoopAnimations(), ], + imports: [ChampionshipTrendComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ChampionshipTrendComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/components/championship-trend/championship-trend.component.ts b/client/src/app/components/championship-trend/championship-trend.component.ts new file mode 100644 index 000000000..5b680771f --- /dev/null +++ b/client/src/app/components/championship-trend/championship-trend.component.ts @@ -0,0 +1,101 @@ +import { NgStyle } from '@angular/common'; +import { CommonModule } from '@angular/common'; +import { Component, DestroyRef, effect, inject, OnInit, Renderer2, signal, WritableSignal, DOCUMENT } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { ChartOptions } from 'chart.js'; +import { + ButtonDirective, + ButtonGroupComponent, + CardBodyComponent, + CardComponent, + CardFooterComponent, + ColComponent, + FormCheckLabelDirective, + GutterDirective, + RowComponent +} from '@coreui/angular'; +import { ChartjsComponent } from '@coreui/angular-chartjs'; +import { DashboardChartsData, IChartProps } from '../../views/dashboard/dashboard-charts-data'; + +@Component({ + selector: 'app-championship-trend', + templateUrl: './championship-trend.component.html', + styleUrls: ['./championship-trend.component.scss'], + imports: [ + CommonModule, + NgStyle, + ReactiveFormsModule, + ChartjsComponent, + CardComponent, + CardBodyComponent, + RowComponent, + ColComponent, + ButtonGroupComponent, + FormCheckLabelDirective, + ButtonDirective, + CardFooterComponent, + GutterDirective + ], + providers: [DashboardChartsData] +}) +export class ChampionshipTrendComponent implements OnInit { + + constructor() {} + + readonly #destroyRef: DestroyRef = inject(DestroyRef); + readonly #document: Document = inject(DOCUMENT); + readonly #renderer: Renderer2 = inject(Renderer2); + readonly #chartsData: DashboardChartsData = inject(DashboardChartsData); + + public mainChart: IChartProps = { type: 'line' }; + public mainChartRef: WritableSignal = signal(undefined); + #mainChartRefEffect = effect(() => { + if (this.mainChartRef()) { + this.setChartStyles(); + } + }); + public trafficRadioGroup = new FormGroup({ + trafficRadio: new FormControl('Year') + }); + + ngOnInit(): void { + this.setTrafficPeriod('Year', 0); + this.updateChartOnColorModeChange(); + } + + initCharts(): void { + this.mainChart = this.#chartsData.mainChart; + } + + setTrafficPeriod(value: string, numberOfRaces: number): void { + this.#chartsData.initMainChart(value, numberOfRaces); + this.initCharts(); + } + + handleChartRef($chartRef: any) { + if ($chartRef) { + this.mainChartRef.set($chartRef); + } + } + + updateChartOnColorModeChange() { + const unListen = this.#renderer.listen(this.#document.documentElement, 'ColorSchemeChange', () => { + this.setChartStyles(); + }); + + this.#destroyRef.onDestroy(() => { + unListen(); + }); + } + + setChartStyles() { + if (this.mainChartRef()) { + setTimeout(() => { + const options: ChartOptions = { ...this.mainChart.options }; + const scales = this.#chartsData.getScales(); + this.mainChartRef().options.scales = { ...options.scales, ...scales }; + this.mainChartRef().update(); + }); + } + } +} \ No newline at end of file diff --git a/client/src/app/components/constructor-card/constructor-card.component.html b/client/src/app/components/constructor-card/constructor-card.component.html new file mode 100644 index 000000000..d30677842 --- /dev/null +++ b/client/src/app/components/constructor-card/constructor-card.component.html @@ -0,0 +1,91 @@ + + + +
+ +
+ {{ constructorData().constructor_name }} +
+
+
+ + + +
+
+
+ {{ constructorData().driver_1_username }}'s avatar +
+
{{ constructorData().driver_1_username }}
+ {{ constructorData().driver_1_tot_points }} Pt. +
+
+
+ {{ constructorData().driver_2_username }}'s avatar +
+
{{ constructorData().driver_2_username }}
+ {{ constructorData().driver_2_tot_points }} Pt. +
+
+
+
+ + +
    +
  • +
    +
    Posizione:
    +
    {{ position() }}°
    +
    +
  • +
  • +
    +
    Tot.:
    +
    {{ constructorData().constructor_tot_points }}
    +
    +
  • +
  • +
    +
    Race:
    +
    {{ constructorData().constructor_race_points }}
    +
    +
  • +
  • +
    +
    Full:
    +
    {{ constructorData().constructor_full_race_points }}
    +
    +
  • +
  • +
    +
    Sprint:
    +
    {{ constructorData().constructor_sprint_points }}
    +
    +
  • +
  • +
    +
    Qualify:
    +
    {{ constructorData().constructor_qualifying_points }}
    +
    +
  • +
  • +
    +
    Practice:
    +
    {{ constructorData().constructor_free_practice_points }}
    +
    +
  • +
+
+
diff --git a/client/src/app/components/constructor-card/constructor-card.component.scss b/client/src/app/components/constructor-card/constructor-card.component.scss new file mode 100644 index 000000000..31068d7ae --- /dev/null +++ b/client/src/app/components/constructor-card/constructor-card.component.scss @@ -0,0 +1,45 @@ +.constructor-card { + margin-bottom: 1rem; + + .constructor-header { + padding: 1rem; + + .constructor-logo { + max-height: 60px; + width: auto; + margin: 0 auto; + } + } + + .drivers-section { + border-bottom: 1px solid #dee2e6; + padding-bottom: 1rem; + margin-bottom: 1rem; + + .driver-avatar { + width: 50px; + height: 50px; + border-radius: 50%; + object-fit: cover; + margin-bottom: 0.5rem; + } + + .driver-info { + h6 { + margin: 0; + font-size: 0.9rem; + } + small { + font-size: 0.8rem; + } + } + } + + .stats-list { + font-size: 0.85rem; + + li { + padding: 0.5rem; + } + } +} diff --git a/client/src/app/components/constructor-card/constructor-card.component.spec.ts b/client/src/app/components/constructor-card/constructor-card.component.spec.ts new file mode 100644 index 000000000..b318bcb2c --- /dev/null +++ b/client/src/app/components/constructor-card/constructor-card.component.spec.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; + +import { ConstructorCardComponent } from './constructor-card.component'; + +describe('ConstructorCardComponent', () => { + let component: ConstructorCardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [provideNoopAnimations(), ], + imports: [ConstructorCardComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ConstructorCardComponent); + component = fixture.componentInstance; + + // Set required inputs + fixture.componentRef.setInput('constructorData', { + constructor_id: 1, + constructor_name: 'Test Constructor', + constructor_color: '#FF0000', + driver_1_id: 1, + driver_1_username: 'driver1', + driver_1_tot_points: 100, + driver_2_id: 2, + driver_2_username: 'driver2', + driver_2_tot_points: 90, + constructor_tot_points: 190 + }); + fixture.componentRef.setInput('position', 1); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/components/constructor-card/constructor-card.component.ts b/client/src/app/components/constructor-card/constructor-card.component.ts new file mode 100644 index 000000000..7cd32aafb --- /dev/null +++ b/client/src/app/components/constructor-card/constructor-card.component.ts @@ -0,0 +1,29 @@ +import { Component, input, ChangeDetectionStrategy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { + CardBodyComponent, + CardComponent, + CardHeaderComponent, + ListGroupDirective, + ListGroupItemDirective +} from '@coreui/angular'; +import type { Constructor } from '@f123dashboard/shared'; + +@Component({ + selector: 'app-constructor-card', + imports: [ + CommonModule, + CardComponent, + CardBodyComponent, + CardHeaderComponent, + ListGroupDirective, + ListGroupItemDirective + ], + templateUrl: './constructor-card.component.html', + styleUrl: './constructor-card.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ConstructorCardComponent { + constructorData = input.required(); + position = input.required(); +} diff --git a/client/src/app/components/countdown/countdown.component.html b/client/src/app/components/countdown/countdown.component.html new file mode 100644 index 000000000..e1686cb19 --- /dev/null +++ b/client/src/app/components/countdown/countdown.component.html @@ -0,0 +1,34 @@ + +@if (targetDate()) { + + +
+ +
+ + TAG Heuer Logo +
+ +
+
{{ timeRemaining().days | number: '2.0' }}
+ GG +
+ +
+
{{ timeRemaining().hours | number: '2.0' }}
+ ORE +
+ +
+
{{ timeRemaining().minutes | number: '2.0' }}
+ MIN +
+ +
+
{{ timeRemaining().seconds | number: '2.0' }}
+ SEC +
+
+
+
+} \ No newline at end of file diff --git a/client/src/app/components/countdown/countdown.component.scss b/client/src/app/components/countdown/countdown.component.scss new file mode 100644 index 000000000..d9a162be4 --- /dev/null +++ b/client/src/app/components/countdown/countdown.component.scss @@ -0,0 +1,36 @@ +.card { + border: none; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + } + + .display-4 { + font-weight: bold; + font-size: 0.7rem; /* Adjust size of the numbers */ + line-height: 1; + } + + small { + font-size: 0.5rem; + text-transform: uppercase; + font-weight: 600; + letter-spacing: 1px; + } + + .time-box { + text-align: center; + } + + .card-body { + display: flex; + justify-content: space-between; + align-items: center; + } + + .clock { + display: flex; /* Imposta il layout flessibile */ + justify-content: center; /* Centra orizzontalmente */ + align-items: center; /* Centra verticalmente */ + margin-top: 10px; /* Mantiene il margine superiore */ + height: 100%; /* Se necessario, assicura che l'altezza del contenitore permetta la centratura verticale */ + } + \ No newline at end of file diff --git a/client/src/app/components/countdown/countdown.component.spec.ts b/client/src/app/components/countdown/countdown.component.spec.ts new file mode 100644 index 000000000..89aca7293 --- /dev/null +++ b/client/src/app/components/countdown/countdown.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; + +import { CountdownComponent } from './countdown.component'; + +describe('CountdownComponent', () => { + let component: CountdownComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [provideNoopAnimations(), ], + imports: [CountdownComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CountdownComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/components/countdown/countdown.component.ts b/client/src/app/components/countdown/countdown.component.ts new file mode 100644 index 000000000..3eb3fcf5b --- /dev/null +++ b/client/src/app/components/countdown/countdown.component.ts @@ -0,0 +1,98 @@ +import { Component, OnInit, DestroyRef, inject, signal, effect, ChangeDetectionStrategy } from '@angular/core'; +import { CommonModule } from '@angular/common'; // Import CommonModule +import { + CardBodyComponent, + CardComponent} from '@coreui/angular'; +import {DbDataService} from '../../service/db-data.service'; +import type { TrackData } from '@f123dashboard/shared'; + +interface TimeInterface { + days: number; + hours: number; + minutes: number; + seconds: number; +} + +@Component({ + selector: 'app-countdown', + imports: [ + CommonModule, + CardComponent, CardBodyComponent + ], + templateUrl: './countdown.component.html', + styleUrl: './countdown.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class CountdownComponent implements OnInit { + private dbData = inject(DbDataService); + private destroyRef = inject(DestroyRef); + + public championshipNextTracks: TrackData[] = []; + + // countdown variables + targetDate = signal(undefined); + timeRemaining = signal({ + days: 0, + hours: 0, + minutes: 0, + seconds: 0 + }); + + ngOnInit(): void { + // countdown to next gp + this.championshipNextTracks = this.dbData.tracks(); + + // filter next championship track + const currentDate = new Date(); + const nextDate = this.championshipNextTracks + .map(track => new Date(track.date)) + .sort((a, b) => a.getTime() - b.getTime()) + .find(dbDate => dbDate >= currentDate); + + if (nextDate) { + this.targetDate.set(nextDate); + this.startCountdown(); + } + } + + startCountdown(): void { + const timer = setInterval(() => { + const target = this.targetDate(); + if (!target) { + return; + } + + const now = new Date().getTime(); + const distance = target.getTime() - now; + + // Calculate time remaining in days, hours, minutes, seconds + const days = Math.floor(distance / (1000 * 60 * 60 * 24)); + const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((distance % (1000 * 60)) / 1000); + + this.timeRemaining.set({ + days, + hours, + minutes, + seconds + }); + + // If the countdown is over, clear the interval + if (distance < 0) { + clearInterval(timer); + this.timeRemaining.set({ + days: 0, + hours: 0, + minutes: 0, + seconds: 0 + }); + } + }, 1000); // Update every second for more accurate countdown + + // Register cleanup + this.destroyRef.onDestroy(() => { + clearInterval(timer); + }); + } +} diff --git a/client/src/app/components/fanta-rules/fanta-rules.component.html b/client/src/app/components/fanta-rules/fanta-rules.component.html new file mode 100644 index 000000000..c1fd67bff --- /dev/null +++ b/client/src/app/components/fanta-rules/fanta-rules.component.html @@ -0,0 +1,42 @@ + + +
Regolamento Punteggi
+
+ +
    +
  • + Posizione Pilota Esatta + {{ exactPositionPoints }} pt +
  • +
  • +
    + Errore di 1 posizione + es. 2° invece di 1° +
    + {{ oneOffPoints }} pt +
  • +
  • +
    + Errore di 2 posizioni + es. 3° invece di 1° +
    + {{ twoOffPoints }} pt +
  • +
  • + Giro Veloce + {{ fastLapPoints }} pt +
  • +
  • +
    + Pilota DNF + Ritirato / Non Partito +
    + {{ dnfPoints }} pt +
  • +
  • + Scuderia Vincente + {{ teamPoints }} pt +
  • +
+
+
diff --git a/client/src/app/components/fanta-rules/fanta-rules.component.scss b/client/src/app/components/fanta-rules/fanta-rules.component.scss new file mode 100644 index 000000000..6e471084c --- /dev/null +++ b/client/src/app/components/fanta-rules/fanta-rules.component.scss @@ -0,0 +1,20 @@ +.rule-text { + font-weight: 500; + color: var(--cui-body-color); +} + +.points-badge { + font-weight: 700; + color: var(--cui-body-color); + font-size: 1rem; + white-space: nowrap; +} + +:host ::ng-deep .list-group-item { + background-color: transparent; + /* Minimal hover effect can be added if desired */ + &:hover { + background-color: rgba(var(--cui-body-color-rgb), 0.03); + } +} + diff --git a/client/src/app/components/fanta-rules/fanta-rules.component.ts b/client/src/app/components/fanta-rules/fanta-rules.component.ts new file mode 100644 index 000000000..d7d6019b6 --- /dev/null +++ b/client/src/app/components/fanta-rules/fanta-rules.component.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { CardModule, ListGroupModule } from '@coreui/angular'; +import { FantaService } from '../../service/fanta.service'; + +@Component({ + selector: 'app-fanta-rules', + imports: [CommonModule, CardModule, ListGroupModule], + templateUrl: './fanta-rules.component.html', + styleUrls: ['./fanta-rules.component.scss'] +}) +export class FantaRulesComponent { + readonly positionPoints = FantaService.CORRECT_RESPONSE_POINTS; + readonly fastLapPoints = FantaService.CORRECT_RESPONSE_FAST_LAP_POINTS; + readonly dnfPoints = FantaService.CORRECT_RESPONSE_DNF_POINTS; + readonly teamPoints = FantaService.CORRECT_RESPONSE_TEAM; + + readonly exactPositionPoints = this.positionPoints[0]; + readonly oneOffPoints = this.positionPoints[1]; + readonly twoOffPoints = this.positionPoints[2]; +} diff --git a/client/src/app/components/leaderboard/leaderboard.component.html b/client/src/app/components/leaderboard/leaderboard.component.html new file mode 100644 index 000000000..f228ce41c --- /dev/null +++ b/client/src/app/components/leaderboard/leaderboard.component.html @@ -0,0 +1,107 @@ + + +
+ + + + NEW! Clicca sul giocatore per vedere le votazioni + + +
+ + + + + + + + @if (showVotes()) { + + } + + + + + @for (leaderBoard of leaderBoards().slice(0, maxDisplayable()); track leaderBoard.id; let i = $index) { + + + + + @if (showVotes()) { + + } + + + } + +
Pos.UsernameVotiPunti
+
+
#{{ i+1 }}
+
+
+ + +
{{ leaderBoard.username }}
+
+
+
+ {{ leaderBoard.numberVotes }} / {{totNumberVotes()}} +
+
+
+ {{ leaderBoard.points }} +
+
+
+ + + + +
+ + Storico Votazioni di {{ selectedUser()?.username }} +
+ +
+ + @if (userVotes().length === 0) { +

Nessuna votazione disponibile

+ } @else { + @for (voteData of userVotes(); track voteData.trackId) { +
+
+ + {{ voteData.trackName }} + +
+ + +
+ @if (!$last) { +
+ } + } + } +
+
diff --git a/client/src/app/components/leaderboard/leaderboard.component.scss b/client/src/app/components/leaderboard/leaderboard.component.scss new file mode 100644 index 000000000..7db7ba3b7 --- /dev/null +++ b/client/src/app/components/leaderboard/leaderboard.component.scss @@ -0,0 +1,21 @@ +.custom-table { + border-collapse: separate; + border-spacing: 0; + overflow: hidden; /* Nasconde i bordi sporgenti */ +} + +// Clickable row styling +tr[style*="cursor: pointer"] { + transition: background-color 0.2s ease; + + &:hover { + background-color: rgba(0, 0, 0, 0.05) !important; + } +} + +// Dark mode support +@media (prefers-color-scheme: dark) { + tr[style*="cursor: pointer"]:hover { + background-color: rgba(255, 255, 255, 0.1) !important; + } +} diff --git a/client/src/app/components/leaderboard/leaderboard.component.spec.ts b/client/src/app/components/leaderboard/leaderboard.component.spec.ts new file mode 100644 index 000000000..9292cc455 --- /dev/null +++ b/client/src/app/components/leaderboard/leaderboard.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; + +import { LeaderboardComponent } from './leaderboard.component'; + +describe('LeaderboardComponent', () => { + let component: LeaderboardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [provideNoopAnimations(), ], + imports: [LeaderboardComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LeaderboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/components/leaderboard/leaderboard.component.ts b/client/src/app/components/leaderboard/leaderboard.component.ts new file mode 100644 index 000000000..a8cd388bd --- /dev/null +++ b/client/src/app/components/leaderboard/leaderboard.component.ts @@ -0,0 +1,142 @@ +import { CommonModule } from '@angular/common'; +import { Component, input, inject, signal, computed, ChangeDetectionStrategy } from '@angular/core'; +import { GridModule, TableDirective, ModalComponent, ModalHeaderComponent, ModalTitleDirective, ModalBodyComponent, ButtonCloseDirective, ThemeDirective, AlertModule } from '@coreui/angular'; +import { LeaderBoard } from '../../../app/model/leaderboard' +import { FantaService } from '../../../app/service/fanta.service'; +import { DbDataService } from '../../../app/service/db-data.service'; +import { cilPeople, cilInfo, cilBell } from '@coreui/icons'; +import { IconDirective } from '@coreui/icons-angular'; +import { AvatarComponent, TextColorDirective } from '@coreui/angular'; +import type { User, FantaVote, TrackData } from '@f123dashboard/shared'; +import { VoteHistoryTableComponent } from '../vote-history-table/vote-history-table.component'; +import { allFlags } from '../../model/constants'; + + +@Component({ + selector: 'app-leaderboard', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + GridModule, + TableDirective, + IconDirective, + TextColorDirective, + AvatarComponent, + ModalComponent, + ModalHeaderComponent, + ModalTitleDirective, + ModalBodyComponent, + ButtonCloseDirective, + ThemeDirective, + VoteHistoryTableComponent, + AlertModule + ], + templateUrl: './leaderboard.component.html', + styleUrl: './leaderboard.component.scss' +}) + +export class LeaderboardComponent { + private fantaService = inject(FantaService); + private dbData = inject(DbDataService); + + maxDisplayable = input(undefined); + showVotes = input(true); + + public cilPeople: string[] = cilPeople; + public cilInfo: string[] = cilInfo; + public cilBell: string[] = cilBell; + public allFlags = allFlags; + + readonly leaderBoards = computed(() => { + return this.dbData.users() + .map(user => ({ + id: user.id, + username: user.username, + points: this.fantaService.getFantaPoints(user.id), + numberVotes: this.fantaService.getFantaNumberVotes(user.id), + avatarImage: user.image + } satisfies LeaderBoard)) + .filter(lb => lb.numberVotes > 0) + .sort((a, b) => b.points - a.points); + }); + + readonly totNumberVotes = this.fantaService.totNumberVotes; + + private modalVisibleSignal = signal(false); + readonly modalVisible = this.modalVisibleSignal.asReadonly(); + + private selectedUserSignal = signal(null); + readonly selectedUser = this.selectedUserSignal.asReadonly(); + + private userVotesSignal = signal<{ vote: FantaVote, trackId: number, trackName: string, trackCountry: string }[]>([]); + readonly userVotes = this.userVotesSignal.asReadonly(); + + getAvatar(userId: number, image?: string): string { + if (image){ + return `data:image/jpeg;base64,${image}`; + } + + // Fallback to file path + return `./assets/images/avatars_fanta/${userId}.png`; + } + + openVoteHistoryModal(userId: number): void { + const users = this.dbData.users(); + const user = users.find(u => u.id === userId); + if (!user) {return;} + + this.selectedUserSignal.set(user); + this.loadUserVotes(userId); + this.modalVisibleSignal.set(true); + } + + private loadUserVotes(userId: number): void { + const tracksWithResults = this.getTracksWithResults(); + const lastTwoTracks = this.getLastTwoTracks(tracksWithResults); + const votes = this.getUserVotesForTracks(userId, lastTwoTracks); + + this.userVotesSignal.set(votes); + } + + private getTracksWithResults() { + const allTracks = this.dbData.tracks(); + return allTracks.filter(track => { + const result = this.fantaService.getRaceResult(track.track_id); + return result && result.id_1_place !== null && result.id_1_place !== undefined; + }); + } + + + private getLastTwoTracks(tracks: TrackData[]): TrackData[] { + const sorted = [...tracks].sort((a, b) => + new Date(b.date).getTime() - new Date(a.date).getTime() + ); + return sorted.slice(0, 2); + } + + private getUserVotesForTracks(userId: number, tracks: TrackData[]): { vote: FantaVote, trackId: number, trackName: string, trackCountry: string }[] { + return tracks + .map(track => { + const vote = this.fantaService.getFantaVote(userId, track.track_id); + return vote ? { + vote, + trackId: track.track_id, + trackName: track.name, + trackCountry: track.country + } : null; + }) + .filter(v => v !== null) as { vote: FantaVote, trackId: number, trackName: string, trackCountry: string }[]; + } + + handleModalVisibilityChange(visible: boolean): void { + if (!visible) { + this.closeModal(); + } + } + + closeModal(): void { + this.modalVisibleSignal.set(false); + this.selectedUserSignal.set(null); + this.userVotesSignal.set([]); + } +} diff --git a/client/src/app/components/login/login.component.html b/client/src/app/components/login/login.component.html new file mode 100644 index 000000000..a34738a2c --- /dev/null +++ b/client/src/app/components/login/login.component.html @@ -0,0 +1,159 @@ + + + + + + + diff --git a/client/src/app/components/login/login.component.scss b/client/src/app/components/login/login.component.scss new file mode 100644 index 000000000..6ccd46e69 --- /dev/null +++ b/client/src/app/components/login/login.component.scss @@ -0,0 +1,333 @@ +// Import common floating form styles +@use '../../../scss/floating-forms'; + +// Login Dropdown Container +.login-dropdown { + position: relative; +} + +// Login Toggle Button +.login-toggle-button { + background: transparent !important; + border: none !important; + padding: 0.5rem 0.75rem; + margin: 0; + border-radius: 50px; + transition: all 0.3s ease; + color: var(--cui-nav-link-color, #8a93a2) !important; + display: flex; + align-items: center; + justify-content: center; + min-width: 40px; + min-height: 40px; + + &:hover { + background: rgba(var(--cui-primary-rgb), 0.1) !important; + color: var(--cui-primary) !important; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + + .login-icon, .user-avatar { + transform: scale(1.1); + } + } + + &:focus { + box-shadow: 0 0 0 2px rgba(var(--cui-primary-rgb), 0.25); + outline: none; + } + + .login-icon { + width: 1.25rem; + height: 1.25rem; + transition: all 0.3s ease; + color: inherit; + } + + .user-avatar { + transition: all 0.3s ease; + border: 2px solid rgba(var(--cui-primary-rgb), 0.2); + } +} + +// Login Dropdown Menu +.login-dropdown-menu { + @extend .modal-dropdown-container; + min-width: 320px !important; + padding: 0; + margin-top: 8px; + + .dropdown-header { + @extend .gradient-header; + + .login-title { + @extend .title; + } + } + + .login-container { + padding: 1.5rem; + } + + .input-group { + @extend .floating-input-group; + margin-bottom: 1rem; // Specific spacing for login dropdown + } + + .error-message { + @extend .form-error-message; + } + + .login-actions { + margin-top: 1.5rem; + display: flex; + justify-content: center; + + .login-button { + @extend .enhanced-button; + width: 100%; + font-size: 0.95rem; + } + } + + .register-section { + padding: 1rem 1.5rem 1.5rem; + text-align: center; + background: var(--cui-tertiary-bg); + + .register-button { + color: var(--cui-primary); + font-weight: 500; + text-decoration: none; + transition: all 0.3s ease; + padding: 0.2rem 1rem; + border-radius: 6px; + + &:hover { + background: rgba(var(--cui-primary-rgb), 0.1); + text-decoration: underline; + } + } + + .forgot-pw-button { + color: var(--cui-tertiary-color); + font-weight: 500; + font-size: 0.7rem; + text-decoration: none; + transition: all 0.3s ease; + padding: 0.2rem 1rem; + border-radius: 6px; + margin-left: 10px; + + &:hover { + background: rgba(var(--cui-tertiary-color-rgb), 0.1); + text-decoration: underline; + } + } + } +} + + // CoreUI Floating Input Groups +.input-floating { + @extend .floating-input-group; +} + +// User Dropdown Menu +.user-dropdown-menu { + @extend .modal-dropdown-container; + min-width: 280px !important; + padding: 0; + margin-top: 8px; + + .user-info-header { + @extend .gradient-header; + padding: 1.5rem; // Override for user info layout + + .user-info { + display: flex; + align-items: center; + gap: 1rem; + + .user-avatar-large { + border: 3px solid rgba(255, 255, 255, 0.3); + flex-shrink: 0; + } + + .user-details { + .user-name { + margin: 0 0 0.25rem 0; + font-size: 1.1rem; + font-weight: 600; + color: white; + + &.clickable-username { + cursor: pointer; + transition: all 0.3s ease; + padding: 0.25rem 0.5rem; + border-radius: 6px; + margin: -0.25rem -0.5rem 0 -0.5rem; + display: inline-block; + + &:hover { + background: rgba(255, 255, 255, 0.15); + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + &:active { + transform: translateY(0); + } + } + } + + .username { + font-size: 0.9rem; + opacity: 0.9; + color: rgba(255, 255, 255, 0.8); + } + } + } + } + + .profile-item { + padding: 1rem 1.5rem; + transition: all 0.3s ease; + cursor: pointer; + border: none; + background: none; + width: 100%; + display: block; + + &:hover { + background: rgba(var(--cui-primary-rgb), 0.1); + color: var(--cui-primary); + } + + .profile-content { + display: flex; + align-items: center; + justify-content: space-between; + + .profile-text { + font-weight: 500; + font-size: 0.95rem; + } + + .profile-icon { + width: 1.1rem; + height: 1.1rem; + transition: transform 0.3s ease; + } + } + + &:hover .profile-icon { + transform: translateX(4px); + } + } + + .password-item { + padding: 1rem 1.5rem; + transition: all 0.3s ease; + cursor: pointer; + border: none; + background: none; + width: 100%; + display: block; + + &:hover { + background: rgba(var(--cui-warning-rgb), 0.1); + color: var(--cui-warning); + } + + .password-content { + display: flex; + align-items: center; + justify-content: space-between; + + .password-text { + font-weight: 500; + font-size: 0.95rem; + } + + .password-icon { + width: 1.1rem; + height: 1.1rem; + transition: transform 0.3s ease; + } + } + + &:hover .password-icon { + transform: translateX(4px); + } + } + + .logout-item { + padding: 1rem 1.5rem; + transition: all 0.3s ease; + cursor: pointer; + border: none; + background: none; + width: 100%; + display: block; + + &:hover { + background: rgba(var(--cui-danger-rgb), 0.1); + color: var(--cui-danger); + } + + .logout-content { + display: flex; + align-items: center; + justify-content: space-between; + + .logout-text { + font-weight: 500; + font-size: 0.95rem; + } + + .logout-icon { + width: 1.1rem; + height: 1.1rem; + transition: transform 0.3s ease; + } + } + + &:hover .logout-icon { + transform: translateX(4px); + } + } +} + +// Legacy styles for backward compatibility +.container { + display: flex; + justify-content: center; + align-items: center; +} + +.avviso { + display: flex; + font-weight: bold; + margin-top: 10px; + margin-left: 10px; + margin-bottom: 0px; +} + +// Responsive Design +@media (max-width: 768px) { + .login-dropdown-menu, + .user-dropdown-menu { + min-width: 280px !important; + margin-left: -120px; + } +} + +.link-name { + display: none; +} + +@media (min-width: 1000px) { + .link-name { + display: inline; + } +} diff --git a/src/app/views/pages/login/login.component.spec.ts b/client/src/app/components/login/login.component.spec.ts similarity index 50% rename from src/app/views/pages/login/login.component.spec.ts rename to client/src/app/components/login/login.component.spec.ts index a612871fd..219d53ba0 100644 --- a/src/app/views/pages/login/login.component.spec.ts +++ b/client/src/app/components/login/login.component.spec.ts @@ -1,27 +1,28 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; -import { ButtonModule, CardModule, FormModule, GridModule } from '@coreui/angular'; import { LoginComponent } from './login.component'; -import { IconModule } from '@coreui/icons-angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; describe('LoginComponent', () => { let component: LoginComponent; let fixture: ComponentFixture; - let iconSetService: IconSetService; beforeEach(async () => { + const activatedRouteStub = { + queryParams: of({}), + snapshot: { queryParams: {} } + }; + await TestBed.configureTestingModule({ - imports: [FormModule, CardModule, GridModule, ButtonModule, IconModule, LoginComponent], - providers: [IconSetService] -}) + providers: [ + provideNoopAnimations(), + { provide: ActivatedRoute, useValue: activatedRouteStub } + ], + imports: [LoginComponent] + }) .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; fixture = TestBed.createComponent(LoginComponent); component = fixture.componentInstance; diff --git a/client/src/app/components/login/login.component.ts b/client/src/app/components/login/login.component.ts new file mode 100644 index 000000000..d0052f4ea --- /dev/null +++ b/client/src/app/components/login/login.component.ts @@ -0,0 +1,208 @@ +import { Component, ViewChild, inject, signal, computed, } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { FormModule } from '@coreui/angular'; +import { AuthService } from './../../service/auth.service'; +import { + AvatarComponent, + DropdownComponent, + DropdownItemDirective, + DropdownMenuDirective, + DropdownToggleDirective, + GridModule, + ButtonDirective, + SpinnerModule, +} from '@coreui/angular'; +import { DbDataService } from '../../../app/service/db-data.service'; +import { cilWarning, cilAccountLogout, cilLockLocked } from '@coreui/icons'; +import { IconDirective } from '@coreui/icons-angular'; +import { cilUser } from '@coreui/icons'; +import { RegistrationModalComponent } from '../registration-modal/registration-modal.component'; +import { PasswordChangeModalComponent } from '../password-change-modal/password-change-modal.component'; +import type { User } from '@f123dashboard/shared'; + +@Component({ + selector: 'login-component', + imports: [ + CommonModule, + FormsModule, + FormModule, + ButtonDirective, + GridModule, + IconDirective, + DropdownComponent, + DropdownToggleDirective, + DropdownMenuDirective, + DropdownItemDirective, + AvatarComponent, + SpinnerModule, + RegistrationModalComponent, + PasswordChangeModalComponent + ], + templateUrl: './login.component.html', + styleUrl: './login.component.scss' +}) +export class LoginComponent { + private authService = inject(AuthService); + private router = inject(Router); + private route = inject(ActivatedRoute); + private dbData = inject(DbDataService); + + icons = { cilUser, cilLockLocked }; + + // Current user data (signals from auth service) + currentUser = this.authService.currentUser; + isAuthenticated = this.authService.isAuthenticated; + + // Login form fields (signals) + username = signal(''); + password = signal(''); + + // State management (signals) + isLoading = signal(false); + errorMessage = signal(''); + + // Validation errors (signals) + usernameError = signal(''); + passwordError = signal(''); + + // Grid gutter configuration (readonly to prevent recreation on change detection) + readonly gutterConfig = { gy: 3 }; + + // Computed values + isLoggedIn = computed(() => !!this.currentUser()); + userDisplayName = computed(() => { + const user = this.currentUser(); + return user ? `${user.name} ${user.surname}` : ''; + }); + userId = computed(() => { + const user = this.currentUser(); + return user ? String(user.id) : 'default'; + }); + avatarSrc = computed(() => this.dbData.getAvatarSrc(this.currentUser())); + + public warningIcon: string[] = cilWarning; + public logoutIcon: string[] = cilAccountLogout; + + @ViewChild('loginDropdown') dropdown!: DropdownComponent; + @ViewChild('registrationModal') registrationModal!: RegistrationModalComponent; + @ViewChild('passwordChangeModal') passwordChangeModal!: PasswordChangeModalComponent; + + async onLogin() { + this.isLoading.set(true); + this.errorMessage.set(''); + if (!this.validateLoginForm()){ + return; + } + try { + // First try login without navigation to check if email is missing + const response = await this.authService.login({ + username: this.username(), + password: this.password() + }, true); // Skip navigation initially + + if (response.success) { + this.dropdown.toggleDropdown(); + + // Check if user has email, if not open registration modal for email completion + if (response.user && (!response.user.mail || response.user.mail.trim() === '')) { + console.log('User email is missing, opening email completion modal.'); + this.isLoading.set(false); + this.errorMessage.set('È necessario completare il profilo inserendo un indirizzo email valido.'); + setTimeout(() => { + this.errorMessage.set(''); // Clear error message before opening modal + this.openEmailCompletionModal(response.user!); + }, 100); // Small delay to ensure modal is ready + return; + } + const user = this.currentUser(); + this.isLoading.set(true); + this.errorMessage.set(''); + + // If email is present, perform navigation manually + const returnUrl = user?.isAdmin ? '/admin' : '/fanta'; + this.router.navigate([returnUrl]); + } else { + this.errorMessage.set(response.message || 'Login fallito. Riprova.'); + } + } catch (error) { + this.errorMessage.set('Si è verificato un errore durante il login. Riprova.'); + console.error('Login error:', error); + } finally { + this.isLoading.set(false); + } + } + + async onLogout() { + try { + await this.authService.logout(); + // Signals will automatically update via toSignal subscriptions + } catch (error) { + console.error('Logout error:', error); + } + } + + private validateLoginForm(): boolean { + this.usernameError.set(''); + this.passwordError.set(''); + + if (!this.username()) { + this.usernameError.set('Nome utente obbligatorio'); + this.isLoading.set(false); + return false; + } + + if (!this.password()) { + this.passwordError.set('Password obbligatoria'); + this.isLoading.set(false); + return false; + } + + return true; + } + + openRegistrationModal() { + this.registrationModal.openForRegistration(); + } + + openEmailCompletionModal(user: User) { + this.registrationModal.openForEmailCompletion(user); + } + + openUserProfileModal() { + const user = this.currentUser(); + if (user) { + this.registrationModal.openForUpdate(user); + } + } + + openPasswordChangeModal() { + this.passwordChangeModal.open(); + } + + onRegistrationSuccess() { + // Gestisce la registrazione avvenuta con successo + // Signals will automatically update via toSignal subscriptions + } + + onUpdateSuccess() { + // Gestisce l'aggiornamento del profilo avvenuto con successo + const user = this.currentUser(); + + // Se l'utente ha appena completato la sua email, segnalo come autenticato e navigo + if (user && user.mail && user.mail.trim() !== '') { + this.authService.markUserAsAuthenticated(); + const returnUrl = user.isAdmin ? '/admin' : '/fanta'; + this.router.navigate([returnUrl]); + } + + // Opzionalmente mostra un messaggio di successo o aggiorna i dati utente + } + + onPasswordChanged() { + // Gestisce il cambio password avvenuto con successo + console.log('Password modificata con successo'); + // Potrebbe mostrare un messaggio di successo o eseguire altre azioni + } +} \ No newline at end of file diff --git a/client/src/app/components/password-change-modal/password-change-modal.component.html b/client/src/app/components/password-change-modal/password-change-modal.component.html new file mode 100644 index 000000000..4ca74af0a --- /dev/null +++ b/client/src/app/components/password-change-modal/password-change-modal.component.html @@ -0,0 +1,173 @@ +@if (visible()) { +
+} + + + + + + +
+ + + + + + + +
+ + +
+
+
+ + + + +
+ + +
+
+
+ + + + +
+ + +
+
+
+ + + @if (errorMessage()) { + + + +
+ Errore! {{ errorMessage() }} +
+
+
+
+ } + + + @if (successMessage()) { + + + +
+ Successo! {{ successMessage() }} +
+
+
+
+ } + + + + +
+ + Requisiti password: +
    +
  • Almeno 8 caratteri
  • +
  • Almeno una lettera maiuscola
  • +
  • Almeno una lettera minuscola
  • +
  • Almeno un numero
  • +
+
+
+
+
+ + + + +
+ + +
+
+
+
+
+
+
diff --git a/client/src/app/components/password-change-modal/password-change-modal.component.scss b/client/src/app/components/password-change-modal/password-change-modal.component.scss new file mode 100644 index 000000000..d5ae55221 --- /dev/null +++ b/client/src/app/components/password-change-modal/password-change-modal.component.scss @@ -0,0 +1,308 @@ +// Import common floating form styles +@use '../../../scss/floating-forms'; + +// Password Change Modal Container +.password-change-modal { + .modal-dialog { + max-width: 500px; + margin: 1.75rem auto; + } + + .modal-content { + border: none; + border-radius: 16px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2); + overflow: hidden; + background: var(--cui-body-bg); + } +} + +// Custom backdrop +.custom-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); + z-index: 1050; +} + +// Modal Header +.modal-header-custom { + padding: 1.5rem 2rem 1rem; + background: linear-gradient(135deg, var(--cui-primary), var(--cui-info)); + color: white; + border-bottom: none; + position: relative; + display: flex; + align-items: center; + justify-content: center; + + .modal-title-custom { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: white; + text-align: center; + } + + .btn-close-custom { + position: absolute; + top: 1rem; + right: 1.5rem; + background: none; + border: none; + color: white; + opacity: 0.8; + transition: all 0.3s ease; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + opacity: 1; + transform: scale(1.1); + } + + &::before { + content: '×'; + font-size: 1.5rem; + font-weight: bold; + } + } +} + +// Modal Body +.modal-body-custom { + padding: 2rem; + background: var(--cui-body-bg); +} + +// Password Change Container +.password-change-container { + width: 100%; + max-width: none; +} + +// Form Styling +.password-change-form { + width: 100%; + + // Input styling using shared floating forms + .input-floating { + margin-bottom: 1.5rem; + + .password-input { + border-radius: 8px; + border: 2px solid var(--cui-border-color); + padding: 1rem 0.75rem 0.25rem 0.75rem; + font-size: 0.95rem; + transition: all 0.3s ease; + background: var(--cui-body-bg); + height: calc(3.5rem + 2px); + line-height: 1.25; + + &:focus { + border-color: var(--cui-primary); + box-shadow: 0 0 0 0.2rem rgba(var(--cui-primary-rgb), 0.25); + background: var(--cui-body-bg); + outline: none; + } + + &:hover:not(:focus) { + border-color: rgba(var(--cui-primary-rgb), 0.5); + } + } + + .input-label { + color: var(--cui-primary); + font-weight: 500; + font-size: 0.85rem; + } + } +} + +// Password Requirements +.password-requirements { + margin-bottom: 1rem; + + .requirements-list { + margin: 0.3rem 0 0 1rem; + padding: 0; + + li { + margin-bottom: 0.275rem; + font-size: 0.65rem; + color: var(--cui-text-muted); + line-height: 1.4; + } + } +} + +// Button Container +.button-container { + display: flex; + justify-content: center; + margin-top: 1.5rem; + gap: 0.5rem; +} + +// Change Password Button +.change-password-button { + padding: 0.875rem 2rem; + border-radius: 10px; + font-size: 1rem; + font-weight: 600; + transition: all 0.3s ease; + width: 100%; + + // Primary button styling (for submit button) + &[color="primary"] { + background: linear-gradient(135deg, var(--cui-primary), var(--cui-info)); + border: none; + color: white; + + &:hover:not(:disabled) { + background: linear-gradient(135deg, + rgba(var(--cui-primary-rgb), 0.9), + rgba(var(--cui-info-rgb), 0.9) + ); + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(var(--cui-primary-rgb), 0.4); + } + + &:disabled { + background: var(--cui-secondary); + color: var(--cui-secondary-color); + cursor: not-allowed; + transform: none; + box-shadow: none; + opacity: 0.7; + } + } + + // Secondary outline button styling (for close button) + &[color="secondary"][variant="outline"] { + &:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(var(--cui-secondary-rgb), 0.3); + } + + &:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; + } + } + + .spinner-border-sm { + width: 1rem; + height: 1rem; + } +} + +// Alert Styling +.error-alert { + color: var(--cui-danger); + font-size: 0.85rem; + font-weight: 500; + margin-top: 0.5rem; + padding: 0.75rem; + background: rgba(var(--cui-danger-rgb), 0.1); + border-radius: 8px; + border-left: 3px solid var(--cui-danger); + margin-bottom: 0; + + .alert-content { + margin: 0; + } +} + +.success-alert { + color: var(--cui-success); + font-size: 0.85rem; + font-weight: 500; + margin-top: 0.5rem; + padding: 0.75rem; + background: rgba(var(--cui-success-rgb), 0.1); + border-radius: 8px; + border-left: 3px solid var(--cui-success); + margin-bottom: 0; + + .alert-content { + margin: 0; + } +} + +// Responsive Design +@media (max-width: 576px) { + .password-change-modal { + .modal-dialog { + margin: 1rem; + max-width: calc(100vw - 2rem); + } + + .modal-body-custom { + padding: 1.5rem 1rem; + } + + .modal-header-custom { + padding: 1.25rem 1rem 0.75rem; + + .modal-title-custom { + font-size: 1.1rem; + } + + .btn-close-custom { + top: 0.75rem; + right: 1rem; + } + } + } + + .change-password-button { + padding: 0.75rem 1.25rem; + font-size: 0.95rem; + } +} + +// Dark mode support +@media (prefers-color-scheme: dark) { + .custom-backdrop { + background: rgba(0, 0, 0, 0.8); + } +} + +// Focus management +.password-change-modal { + &.show { + .password-input:first-of-type { + // Auto-focus first input when modal opens + outline: none; + } + } +} + +// Animation enhancements +.modal-content { + transition: all 0.3s ease; +} + +.password-change-modal.show .modal-content { + animation: modalSlideIn 0.3s ease-out; +} + +@keyframes modalSlideIn { + from { + opacity: 0; + transform: translateY(-50px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} diff --git a/client/src/app/components/password-change-modal/password-change-modal.component.spec.ts b/client/src/app/components/password-change-modal/password-change-modal.component.spec.ts new file mode 100644 index 000000000..f7e7c4067 --- /dev/null +++ b/client/src/app/components/password-change-modal/password-change-modal.component.spec.ts @@ -0,0 +1,76 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { signal } from '@angular/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { FormsModule } from '@angular/forms'; +import { AuthService } from '../../service/auth.service'; +import { PasswordChangeModalComponent } from './password-change-modal.component'; + +describe('PasswordChangeModalComponent', () => { + let component: PasswordChangeModalComponent; + let fixture: ComponentFixture; + let mockAuthService: jasmine.SpyObj; + + beforeEach(async () => { + const authSpy = jasmine.createSpyObj('AuthService', ['changePassword', 'isPasswordStrong'], { currentUser: signal(null) }); + + await TestBed.configureTestingModule({ + imports: [PasswordChangeModalComponent, + FormsModule + ], + providers: [provideNoopAnimations(), { provide: AuthService, useValue: authSpy } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(PasswordChangeModalComponent); + component = fixture.componentInstance; + mockAuthService = TestBed.inject(AuthService) as jasmine.SpyObj; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should open and close modal', () => { + expect(component.visible()).toBeFalsy(); + + component.open(); + expect(component.visible()).toBeTruthy(); + + component.close(); + expect(component.visible()).toBeFalsy(); + }); + + it('should check password strength via authService', () => { + mockAuthService.isPasswordStrong.and.callFake((password: string) => { + const hasUppercase = /[A-Z]/.test(password); + const hasLowercase = /[a-z]/.test(password); + const hasNumber = /[0-9]/.test(password); + return hasUppercase && hasLowercase && hasNumber; + }); + + expect(mockAuthService.isPasswordStrong('weak')).toBeFalsy(); + expect(mockAuthService.isPasswordStrong('Weak123')).toBeTruthy(); + expect(mockAuthService.isPasswordStrong('weak123')).toBeFalsy(); + expect(mockAuthService.isPasswordStrong('WEAK123')).toBeFalsy(); + expect(mockAuthService.isPasswordStrong('WeakPass')).toBeFalsy(); + expect(mockAuthService.isPasswordStrong('Strong123')).toBeTruthy(); + }); + + it('should reset form on close', () => { + component.currentPassword.set('test'); + component.newPassword.set('test123'); + component.confirmPassword.set('test123'); + component.errorMessage.set('Error'); + component.visible.set(true); + + component.close(); + + expect(component.currentPassword()).toBe(''); + expect(component.newPassword()).toBe(''); + expect(component.confirmPassword()).toBe(''); + expect(component.errorMessage()).toBe(''); + expect(component.visible()).toBeFalsy(); + }); +}); diff --git a/client/src/app/components/password-change-modal/password-change-modal.component.ts b/client/src/app/components/password-change-modal/password-change-modal.component.ts new file mode 100644 index 000000000..0368dce33 --- /dev/null +++ b/client/src/app/components/password-change-modal/password-change-modal.component.ts @@ -0,0 +1,150 @@ +import { Component, inject, signal, computed, output, viewChild, ChangeDetectionStrategy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { + ButtonDirective, + FormModule, + GridModule, + ModalBodyComponent, + ModalComponent, + ModalHeaderComponent, + SpinnerComponent, + AlertComponent +} from '@coreui/angular'; +import { AuthService } from '../../service/auth.service'; + +@Component({ + selector: 'app-password-change-modal', + templateUrl: './password-change-modal.component.html', + styleUrls: ['./password-change-modal.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + FormsModule, + FormModule, + ButtonDirective, + GridModule, + ModalComponent, + ModalHeaderComponent, + ModalBodyComponent, + SpinnerComponent, + AlertComponent + ] +}) +export class PasswordChangeModalComponent { + private authService = inject(AuthService); + + modal = viewChild('passwordChangeModal'); + passwordChanged = output(); + + // Property to control visibility + visible = signal(false); + + // Form fields + currentPassword = signal(''); + newPassword = signal(''); + confirmPassword = signal(''); + + // UI state + isLoading = signal(false); + errorMessage = signal(''); + successMessage = signal(''); + + currentUser = computed(() => this.authService.currentUser()); + + public open(): void { + this.resetForm(); + this.visible.set(true); + } + + public close(): void { + this.visible.set(false); + this.resetForm(); + } + + // method to handle two-way binding + handleVisibilityChange(event: boolean) { + this.visible.set(event); + } + + private resetForm(): void { + this.currentPassword.set(''); + this.newPassword.set(''); + this.confirmPassword.set(''); + this.errorMessage.set(''); + this.successMessage.set(''); + this.isLoading.set(false); + } + + private validateForm(): boolean { + this.errorMessage.set(''); + + if (!this.currentPassword().trim()) { + this.errorMessage.set('La password attuale è obbligatoria'); + return false; + } + + if (!this.newPassword().trim()) { + this.errorMessage.set('La nuova password è obbligatoria'); + return false; + } + + if (this.newPassword().length < 8) { + this.errorMessage.set('La nuova password deve essere di almeno 8 caratteri'); + return false; + } + + if (!this.authService.isPasswordStrong(this.newPassword())) { + this.errorMessage.set('La password deve contenere almeno una lettera maiuscola, una lettera minuscola e un numero'); + return false; + } + + if (this.newPassword() !== this.confirmPassword()) { + this.errorMessage.set('Le password non corrispondono'); + return false; + } + + if (this.currentPassword() === this.newPassword()) { + this.errorMessage.set('La nuova password deve essere diversa da quella attuale'); + return false; + } + + return true; + } + + async onChangePassword(): Promise { + if (!this.validateForm()) { + return; + } + + this.isLoading.set(true); + this.errorMessage.set(''); + this.successMessage.set(''); + + try { + + const response = await this.authService.changePassword(this.currentPassword(), this.newPassword()); + + if (response.success) { + this.successMessage.set('Password cambiata con successo!'); + this.passwordChanged.emit(); + + // Close modal after a brief delay to show success message + setTimeout(() => { + this.close(); + }, 2000); + } else { + this.errorMessage.set(response.message || 'Errore durante il cambio password'); + } + } catch (error) { + console.error('Password change error:', error); + this.errorMessage.set('Si è verificato un errore durante il cambio password. Riprova più tardi.'); + } finally { + this.isLoading.set(false); + } + } + + onModalHidden(): void { + this.resetForm(); + } +} diff --git a/client/src/app/components/pilot-card/pilot-card.component.html b/client/src/app/components/pilot-card/pilot-card.component.html new file mode 100644 index 000000000..cad246823 --- /dev/null +++ b/client/src/app/components/pilot-card/pilot-card.component.html @@ -0,0 +1,76 @@ + + + + {{ pilota().driver_username }}'s avatar + +
{{ pilota().driver_username }}
+

{{ pilota().driver_description }}

+
+
    +
  • +
    +
    Posizione:
    +
    {{ position() }}°
    +
    +
  • +
  • +
    +
    Tot. points:
    +
    {{ pilota().total_points }} Pt.
    +
    +
  • +
  • +
    +
    Race:
    +
    {{ pilota().total_race_points }} Pt.
    +
    +
  • +
  • +
    +
    Full Race:
    +
    {{ pilota().total_full_race_points }} Pt.
    +
    +
  • +
  • +
    +
    Sprint:
    +
    {{ pilota().total_sprint_points }} Pt.
    +
    +
  • +
  • +
    +
    Qualify:
    +
    {{ pilota().total_qualifying_points }} Pt.
    +
    +
  • +
  • +
    +
    Practice:
    +
    {{ pilota().total_free_practice_points }} Pt.
    +
    +
  • +
  • +
    +
    License:
    +
    {{ pilota().driver_license_pt }} Pt.
    +
    +
  • +
+ + +
+ + +
+ +
+
+
diff --git a/client/src/app/components/pilot-card/pilot-card.component.scss b/client/src/app/components/pilot-card/pilot-card.component.scss new file mode 100644 index 000000000..36c5ba410 --- /dev/null +++ b/client/src/app/components/pilot-card/pilot-card.component.scss @@ -0,0 +1,84 @@ +// Variabili per la personalizzazione rapida +$card-inner-padding: 0rem; // Padding interno delle card +$card-text-size: 0.875rem; // Dimensione testo base (14px) +$card-title-size: 0.9rem; // Dimensione titolo (16px) +$card-description-height: 65px; // Altezza sezione descrizione +$list-item-padding: 0; // Padding delle voci della lista + +// Stili della card +.pilot-card { + margin-bottom: 0.5rem; + margin-left: 0.5rem; + margin-right: 0.5rem; + + // Header della card con immagine e descrizione + .card-header { + padding: $card-inner-padding; + + img { + width: 70%; + height: auto; + display: block; + margin: 5px auto; + } + + .card-avatar { + border-radius: 50%; /* makes it round (circle/ellipse) */ + } + + .card-body { + height: $card-description-height; + padding: $card-inner-padding; + + h5 { + font-size: $card-title-size; + margin-bottom: 0.25rem; + } + + p { + font-size: $card-text-size; + margin-bottom: 0; + } + } + } + + // Lista delle statistiche + .stats-list { + font-size: $card-text-size; + + .list-group-item { + padding: $list-item-padding $card-inner-padding; + + .row { + margin: 0; + + > div { + padding: 0.1rem; + + &.border-end { + border-right-width: 1px; + } + } + } + } + } +} + +// Stili del grafico radar +.radar-chart-container { + padding: $card-inner-padding; + aspect-ratio: 1; + width: 100%; + max-height: 150px; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + + c-chart { + width: 100% !important; + height: 100% !important; + max-width: 100%; + margin: 0; + } +} diff --git a/client/src/app/components/pilot-card/pilot-card.component.spec.ts b/client/src/app/components/pilot-card/pilot-card.component.spec.ts new file mode 100644 index 000000000..50cc53ab0 --- /dev/null +++ b/client/src/app/components/pilot-card/pilot-card.component.spec.ts @@ -0,0 +1,50 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; + +import { PilotCardComponent } from './pilot-card.component'; + +describe('PilotCardComponent', () => { + let component: PilotCardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [provideNoopAnimations(), ], + imports: [PilotCardComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PilotCardComponent); + component = fixture.componentInstance; + + // Set required inputs + fixture.componentRef.setInput('pilota', { + driver_id: 1, + driver_username: 'testdriver', + driver_name: 'Test', + driver_surname: 'Driver', + driver_description: 'Test description', + driver_license_pt: 3, + driver_consistency_pt: 5, + driver_fast_lap_pt: 4, + drivers_dangerous_pt: 2, + driver_ingenuity_pt: 3, + driver_strategy_pt: 4, + driver_color: '#FF0000', + car_name: 'Test Car', + car_overall_score: 85, + total_sprint_points: 10, + total_free_practice_points: 15, + total_qualifying_points: 20, + total_full_race_points: 25, + total_race_points: 30, + total_points: 100 + }); + fixture.componentRef.setInput('position', 1); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/components/pilot-card/pilot-card.component.ts b/client/src/app/components/pilot-card/pilot-card.component.ts new file mode 100644 index 000000000..e8c196367 --- /dev/null +++ b/client/src/app/components/pilot-card/pilot-card.component.ts @@ -0,0 +1,104 @@ +import { Component, input, computed, ChangeDetectionStrategy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { + CardBodyComponent, + CardComponent, + CardImgDirective, + CardTextDirective, + CardTitleDirective, + ListGroupDirective, + ListGroupItemDirective +} from '@coreui/angular'; +import { ChartjsComponent } from '@coreui/angular-chartjs'; +import type { DriverData } from '@f123dashboard/shared'; + +@Component({ + selector: 'app-pilot-card', + imports: [ + CommonModule, + CardComponent, + CardBodyComponent, + CardTitleDirective, + CardTextDirective, + ListGroupDirective, + ListGroupItemDirective, + CardImgDirective, + ChartjsComponent + ], + templateUrl: './pilot-card.component.html', + styleUrl: './pilot-card.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PilotCardComponent { + pilota = input.required(); + position = input.required(); + + // Variabili per la personalizzazione del radar chart + private readonly CHART_TEXT_COLOR = 'rgba(130, 130, 130, 1)'; + private readonly CHART_TEXT_SIZE = 12; + + // Opzioni per il radar chart + radarChartOptions: any = { + responsive: true, + maintainAspectRatio: false, + scales: { + r: { + beginAtZero: true, + max: 5, + grid: { + color: 'rgba(130, 130, 130, 1)', + lineWidth: 1, + }, + ticks: { + display: false, + stepSize: 1, + }, + pointLabels: { + color: this.CHART_TEXT_COLOR, + font: { + size: this.CHART_TEXT_SIZE, + weight: 'normal' + } + } + } + }, + elements: { + line: { + borderWidth: 1, + } + }, + layout: { + padding: 5 + }, + plugins: { + legend: { + display: false, + position: 'top' + }, + title: { + display: false, + text: 'Performance Radar Chart' + } + } + }; + + radarChartData = computed(() => ({ + labels: ['Costanza', 'Veloce', 'Rischio', 'Errori', 'Tattica'], + datasets: [ + { + label: this.pilota().driver_name, + backgroundColor: 'rgba(255,181,198,0.3)', + borderColor: 'rgba(255,180,180,0.8)', + pointBackgroundColor: 'rgba(255,180,180,0.8)', + pointBorderColor: 'rgba(255,180,180,0.8)', + data: [ + this.pilota().driver_consistency_pt, + this.pilota().driver_fast_lap_pt, + this.pilota().drivers_dangerous_pt, + this.pilota().driver_ingenuity_pt, + this.pilota().driver_strategy_pt + ] + } + ] + })); +} diff --git a/client/src/app/components/podium-card/podium-card.component.html b/client/src/app/components/podium-card/podium-card.component.html new file mode 100644 index 000000000..525a28def --- /dev/null +++ b/client/src/app/components/podium-card/podium-card.component.html @@ -0,0 +1,66 @@ + +
+

{{ championshipTitle() }}

+
+ +
+ @for (pilota of podio(); track pilota.nome; let i = $index) { +
+ {{ i + 1 }} + +
+
+ +
+ + + {{ pilota.punti }} pt + + +
+
+ {{ pilota.nome }} +
+
+
+
+ } +
+ +
+ + + @for (p of classifica(); track p.nome; let i = $index) { + + + + + + } + +
#{{ podio().length + i + 1 }}{{ p.nome }}{{ p.punti }} pt
+
+
diff --git a/client/src/app/components/podium-card/podium-card.component.scss b/client/src/app/components/podium-card/podium-card.component.scss new file mode 100644 index 000000000..de11c7af3 --- /dev/null +++ b/client/src/app/components/podium-card/podium-card.component.scss @@ -0,0 +1,390 @@ +:host { + display: block; +} + +.albo-container { + position: relative; + display: grid; + grid-template-rows: auto auto minmax(0, 1fr); + gap: clamp(0.9rem, 1.75cqi, 1.35rem); + aspect-ratio: 1500 / 1861; + container-type: inline-size; + overflow: hidden; + min-width: 0; + padding: clamp(5.25rem, 18.2cqi, 8.5rem) clamp(0.95rem, 4.1cqi, 2rem) clamp(0.95rem, 4.1cqi, 2rem); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 24px; + color: #fff; + background: + linear-gradient(180deg, rgba(0, 0, 0, 0.06) 0%, rgba(0, 0, 0, 0.24) 100%), + url("/assets/images/albo d'oro/sfondo.png") center/100% 100% no-repeat; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.08), + 0 24px 50px rgba(0, 0, 0, 0.32); + + &::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(180deg, rgba(0, 0, 0, 0.02) 0%, rgba(0, 0, 0, 0.1) 62%, rgba(0, 0, 0, 0.18) 100%); + pointer-events: none; + } +} + +.albo-header { + position: relative; + z-index: 1; + display: flex; + justify-content: center; + text-align: center; +} + +.albo-header h1 { + margin: 0; + max-width: 88%; + font-size: clamp(1.55rem, 5cqi, 3rem); + font-weight: 900; + line-height: 0.96; + text-wrap: balance; + text-shadow: 0 3px 18px rgba(0, 0, 0, 0.42); +} + +.podio { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.12fr) minmax(0, 0.92fr); + align-items: end; + gap: clamp(0.45rem, 1.6cqi, 1rem); + min-height: 0; +} + +.podio-slot { + position: relative; + min-width: 0; +} + +.podio-card { + --accent: var(--driver-accent, #8f8f8f); + --accent-top: color-mix(in srgb, var(--accent) 78%, white 22%); + --accent-mid: color-mix(in srgb, var(--accent) 92%, black 8%); + --accent-bottom: color-mix(in srgb, var(--accent) 56%, black 44%); + --accent-line: color-mix(in srgb, var(--accent) 56%, white 44%); + --footer-height: clamp(2.9rem, 5.8cqi, 4.15rem); + --score-size: clamp(2rem, 4.1cqi, 2.95rem); + --image-scale: 0.92; + --image-shift: 0%; + position: relative; + isolation: isolate; + height: clamp(16.5rem, 49cqi, 31rem); + min-width: 0; + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--accent) 38%, white 62%); + border-radius: 0; + clip-path: polygon(14% 0, 100% 0, 100% 100%, 0 100%, 0 12%); + background: linear-gradient( + 180deg, + var(--accent-top) 0%, + var(--accent-mid) 42%, + var(--accent-bottom) 100% + ); + box-shadow: + 0 1.6cqi 3.2cqi rgba(0, 0, 0, 0.32), + inset 0 -0.85cqi 0 rgba(0, 0, 0, 0.12); + + &::before { + content: ''; + position: absolute; + inset: 0 0 var(--footer-height) 0; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0.03) 24%, rgba(0, 0, 0, 0.12) 100%), + url("/assets/images/albo d'oro/texture.png") center 48% / 200% no-repeat; + mix-blend-mode: soft-light; + opacity: 0.5; + pointer-events: none; + } + + &.first { + --image-scale: 0.96; + height: clamp(19.5rem, 58cqi, 35rem); + z-index: 2; + } + + &.second { + --image-scale: 0.88; + --image-shift: -1%; + margin-bottom: clamp(1.35rem, 4.6cqi, 2.6rem); + } + + &.third { + --image-scale: 0.84; + --image-shift: 1%; + height: clamp(15.4rem, 45cqi, 28.5rem); + margin-bottom: clamp(1rem, 3.4cqi, 1.9rem); + } +} + +.numero { + position: absolute; + top: clamp(-0.85rem, -1cqi, -0.2rem); + left: clamp(0.35rem, 0.8cqi, 0.65rem); + z-index: 3; + font-size: clamp(4.9rem, 16.8cqi, 9.25rem); + font-weight: 900; + line-height: 0.9; + letter-spacing: -0.08em; + color: rgba(255, 255, 255, 0.96); + text-shadow: 0.05em 0.1em 0.1em rgba(0, 0, 0, 0.438); +} + +.podio-portrait { + position: absolute; + inset: clamp(1rem, 3.2cqi, 1.8rem) -1% calc(var(--footer-height) + clamp(0.6rem, 1.8cqi, 1.05rem)) 0; + z-index: 0; + display: flex; + align-items: flex-end; + justify-content: center; +} + +.pilota-img { + width: 82%; + height: 100%; + object-fit: contain; + object-position: bottom center; + transform: translateX(var(--image-shift)) scale(var(--image-scale)); + transform-origin: bottom center; + filter: drop-shadow(0 0.8cqi 1.5cqi rgba(0, 0, 0, 0.36)); +} + +.info { + position: absolute; + inset: auto 0 0; + z-index: 2; + height: var(--footer-height); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--accent) 80%, black 20%) 0%, + color-mix(in srgb, var(--accent) 64%, black 36%) 100% + ); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.18), + inset 0 -0.35rem 0 var(--accent-line); + pointer-events: none; +} + +.nameplate { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: flex-start; + min-width: 0; + padding: clamp(0.45rem, 1.2cqi, 0.8rem) clamp(0.75rem, 1.65cqi, 1.1rem); + text-align: left; +} + +.nome { + display: -webkit-box; + width: 100%; + overflow: hidden; + font-size: clamp(01rem, 0.7vw, 1.2rem); + font-weight: 900; + line-height: 1.3; + letter-spacing: 0.02em; + white-space: normal; + overflow-wrap: anywhere; + line-clamp: 1; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.28); +} + +.punti { + position: absolute; + right: clamp(0.28rem, 0.85cqi, 0.7rem); + bottom: calc(var(--footer-height) + clamp(0.18rem, 0.6cqi, 0.45rem)); + z-index: 2; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: var(--score-size); + height: var(--score-size); + padding: 0 clamp(0.6rem, 0.6cqi, 0.45rem); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(228, 228, 228, 0.98) 100%); + color: #111; + font-size: clamp(0.75rem, 2.5cqi, 0.98rem); + font-weight: 900; + line-height: 1; + white-space: nowrap; + border: 1px solid rgba(0, 0, 0, 0.16); + border-radius: 0.4rem; + box-shadow: 0 0.3cqi 0.8cqi rgba(0, 0, 0, 0.18); + + span { + display: block; + } +} + +.classifica { + position: relative; + z-index: 1; + min-height: 0; + overflow: auto; + align-self: stretch; + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.38) transparent; +} + +.classifica::-webkit-scrollbar { + width: 8px; +} + +.classifica::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.38); +} + +.classifica table { + width: 100%; + margin: 0; + table-layout: fixed; + border-collapse: separate; + border-spacing: 0; + overflow: hidden; + color: #111111 !important; + border: 1px solid rgba(185, 185, 185, 0.75); + background: #f4f4f4 !important; + box-shadow: 0 0.9cqi 1.8cqi rgba(0, 0, 0, 0.22); +} + +.classifica tr { + background: + linear-gradient(180deg, #ddddce 0%, #ddddce 100%); + background-blend-mode: multiply; + color: #111111 !important; + + &:nth-child(even) { + background: + linear-gradient(180deg, #b8b8b8 0%, #b8b8b8 100%); + } +} + +.classifica td { + padding: clamp(0.45rem, 1.2cqi, 0.85rem) clamp(0.4rem, 1.15cqi, 0.85rem); + border-top: 1px solid rgba(0, 0, 0, 0.09); + vertical-align: middle; + background-color: transparent !important; + color: #111111 !important; +} + +.classifica tr:first-child td { + border-top: 0; +} + +.pos { + width: 17%; + font-size: clamp(1.15rem, 2.9cqi, 2.1rem); + font-weight: 900; + line-height: 1; + white-space: nowrap; +} + +.nome-table { + width: 58%; + overflow: hidden; + font-size: clamp(0.95rem, 2.35cqi, 1.45rem); + font-weight: 900; + line-height: 1.05; + text-overflow: ellipsis; + white-space: nowrap; +} + +.punti-table { + width: 25%; + text-align: right; + font-size: clamp(0.82rem, 2.05cqi, 1.25rem); + font-weight: 900; + white-space: nowrap; +} + +.classifica tbody, +.classifica tbody tr:hover, +.classifica tbody tr:hover td { + color: #111111 !important; +} + +@media (max-width: 767px) { + .albo-container { + gap: 0.8rem; + padding-top: clamp(4.4rem, 17.2cqi, 5.8rem); + padding-right: clamp(0.75rem, 3.7cqi, 1rem); + padding-bottom: clamp(0.75rem, 3.7cqi, 1rem); + padding-left: clamp(0.75rem, 3.7cqi, 1rem); + } + + .albo-header h1 { + max-width: 92%; + font-size: clamp(1.3rem, 4.5cqi, 1.85rem); + } + + .podio { + gap: 0.45rem; + } + + .podio-card, + .podio-card.first, + .podio-card.third { + height: clamp(12.3rem, 46cqi, 16.2rem); + } + + .podio-card.second { + margin-bottom: clamp(0.65rem, 2.8cqi, 1.15rem); + } + + .podio-card.third { + margin-bottom: clamp(0.35rem, 1.7cqi, 0.75rem); + } + + .numero { + font-size: clamp(3.2rem, 13.5cqi, 5rem); + } + + .podio-portrait { + inset: clamp(0.55rem, 2.2cqi, 0.95rem) -2% calc(var(--footer-height) + clamp(0.35rem, 1.2cqi, 0.7rem)) 0; + } + + .pilota-img { + width: 80%; + } + + .nameplate { + padding-right: clamp(0.45rem, 1.6cqi, 0.7rem); + padding-left: clamp(0.45rem, 1.6cqi, 0.7rem); + } + + .nome { + font-size: clamp(0.60rem, 3vw, 0.98rem); + line-clamp: 1; + -webkit-line-clamp: 1; + text-overflow: ellipsis; + } + + .punti { + font-size: clamp(0.75rem, 2.3cqi, 0.9rem); + } + + .classifica td { + padding: clamp(0.35rem, 1.2cqi, 0.55rem) clamp(0.3rem, 1cqi, 0.55rem); + } + + .pos { + font-size: clamp(0.92rem, 2.9cqi, 1.25rem); + } + + .nome-table { + font-size: clamp(0.76rem, 2.35cqi, 1rem); + } + + .punti-table { + font-size: clamp(0.66rem, 2.1cqi, 0.9rem); + } +} diff --git a/client/src/app/components/podium-card/podium-card.component.spec.ts b/client/src/app/components/podium-card/podium-card.component.spec.ts new file mode 100644 index 000000000..b92993271 --- /dev/null +++ b/client/src/app/components/podium-card/podium-card.component.spec.ts @@ -0,0 +1,26 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { PodiumCardComponent } from './podium-card.component'; + +describe('PodiumCardComponent', () => { + let component: PodiumCardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [provideNoopAnimations(), ], + imports: [PodiumCardComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PodiumCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/components/podium-card/podium-card.component.ts b/client/src/app/components/podium-card/podium-card.component.ts new file mode 100644 index 000000000..2547eca20 --- /dev/null +++ b/client/src/app/components/podium-card/podium-card.component.ts @@ -0,0 +1,44 @@ +import { Component, input, ChangeDetectionStrategy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { CardModule, TableModule } from '@coreui/angular'; +import { TableDirective } from '@coreui/angular'; + +interface PodiumEntry { + nome: string; + img: string; + colore: string; + punti: string; +} + +interface ClassificaEntry { + nome: string; + punti: string; +} + +@Component({ + selector: 'app-podium-card', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, CardModule, TableModule, TableDirective], + templateUrl: './podium-card.component.html', + styleUrls: ['./podium-card.component.scss'] +}) +export class PodiumCardComponent { + podio = input([]); + classifica = input([]); + championshipTitle = input(''); + + private readonly fallbackPalette = ['#d34fa4', '#5f9de5', '#f0c419', '#ff7a18', '#3bb273', '#7c5cff']; + + getDriverAccentColor(entry: PodiumEntry): string { + const providedColor = entry.colore?.trim(); + if (providedColor) { + return providedColor; + } + + const hash = entry.nome + .split('') + .reduce((acc, char) => ((acc << 5) - acc + char.charCodeAt(0)) | 0, 0); + + return this.fallbackPalette[Math.abs(hash) % this.fallbackPalette.length]; + } +} diff --git a/client/src/app/components/registration-modal/registration-modal.component.html b/client/src/app/components/registration-modal/registration-modal.component.html new file mode 100644 index 000000000..b258dd79f --- /dev/null +++ b/client/src/app/components/registration-modal/registration-modal.component.html @@ -0,0 +1,227 @@ +@if (visible()) { +
+} + + + + + + + @if (isEmailCompletion()) { + + Profilo incompleto! Per accedere alle funzionalità complete dell'applicazione, è necessario fornire un indirizzo email valido. + + } + +
+ + + + + + +
+ + +
+
+ +
+ + +
+
+
+ + + +
+ + +
+
+
+ + + +
+ + +
+
+
+ + + @if (mode() === 'register') { + + +
+ + +
+
+ +
+ + +
+
+
+ } + + + +
+ + + @if (selectedFile()) { +
+
+ Profile preview +
+
+ {{ selectedFile()!.name }} + +
+
+ } +
+
+
+ + @if (singInErrorMessage()) { +
+ + + + {{ singInErrorMessage() }} +
+ } + + @if (successMessage()) { +
+ + + + {{ successMessage() }} +
+ } + +
+ + +
+
+
+
+
diff --git a/client/src/app/components/registration-modal/registration-modal.component.scss b/client/src/app/components/registration-modal/registration-modal.component.scss new file mode 100644 index 000000000..96af4fd26 --- /dev/null +++ b/client/src/app/components/registration-modal/registration-modal.component.scss @@ -0,0 +1,262 @@ +// Import common floating form styles +@use '../../../scss/floating-forms'; + +// Custom backdrop +.custom-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + z-index: 1054; + backdrop-filter: blur(4px); +} + +// Registration Modal +.registration-modal { + .modal-dialog { + max-width: 600px; + margin: 2rem auto; + } + + .modal-content { + @extend .modal-dropdown-container; + @extend .enhanced; + } +} + +// Modal Header +.modal-header-custom { + @extend .gradient-header; + padding: 2rem 2rem 1rem; + + .modal-title-custom { + @extend .title; + font-size: 1.5rem; + width: 100%; + } +} + +// Modal Body +.modal-body-custom { + padding: 0; +} + +// Registration Form +.registration-form { + .registration-container { + padding: 2rem; + } + + // CoreUI Floating Input Groups + .input-floating { + @extend .floating-input-group; + + // Readonly input styling for update mode + .readonly-input { + background-color: var(--cui-gray-100) !important; + color: var(--cui-gray-600) !important; + cursor: not-allowed; + opacity: 0.8; + + &:focus { + box-shadow: none !important; + border-color: var(--cui-gray-300) !important; + } + } + } + + // Legacy input-group class for compatibility + .input-group { + @extend .floating-input-group; + } + + // File Input Styling + .file-input-group { + @extend .file-input-group; + } + + // File Selected with Preview + .file-selected { + display: flex; + align-items: center; + gap: 1rem; + margin-top: 0.75rem; + padding: 0.75rem; + background: rgba(var(--cui-primary-rgb), 0.1); + border: 2px solid rgba(var(--cui-primary-rgb), 0.3); + border-radius: 0.75rem; + + .image-preview-container { + flex-shrink: 0; + + .image-preview { + width: 60px; + height: 60px; + border-radius: 50%; + object-fit: cover; + border: 2px solid rgba(var(--cui-primary-rgb), 0.5); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + } + } + + .file-info { + flex-grow: 1; + display: flex; + align-items: center; + justify-content: space-between; + + .file-name { + color: var(--cui-body-color); + font-size: 0.875rem; + font-weight: 500; + word-break: break-word; + } + + .remove-file { + background: rgba(var(--cui-danger-rgb), 0.1); + color: var(--cui-danger); + border: none; + border-radius: 50%; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; + + &:hover { + background: var(--cui-danger); + color: white; + transform: scale(1.1); + } + } + } + } + + // Error Messages + .error-message { + @extend .form-error-message; + margin: 1rem 0; + } + + // Success Messages + .success-message { + @extend .form-success-message; + margin: 1rem 0; + } + + // Registration Actions + .registration-actions { + margin-top: 2rem; + display: flex; + justify-content: center; + + .registration-button { + @extend .enhanced-button; + @extend .large; + width: 100%; + + .me-2 { + margin-right: 0.5rem; + } + } + } +} + +// Responsive Design +@media (max-width: 768px) { + .registration-modal { + .modal-dialog { + margin: 1rem; + max-width: calc(100% - 2rem); + } + } + + .modal-header-custom { + padding: 1.5rem 1.5rem 0.75rem; + + .modal-title-custom { + font-size: 1.25rem; + } + } + + .registration-form { + .registration-container { + padding: 1.5rem; + } + + .input-group { + margin-bottom: 1rem; + } + + .registration-actions { + margin-top: 1.5rem; + } + } +} + +@media (max-width: 576px) { + .modal-header-custom { + padding: 1.25rem 1.25rem 0.5rem; + + .modal-title-custom { + font-size: 1.1rem; + } + } + + .registration-form { + .registration-container { + padding: 1.25rem; + } + + .input-group { + margin-bottom: 0.875rem; + + .registration-input { + padding: 0.625rem 0.875rem; + font-size: 0.9rem; + } + + .input-label { + font-size: 0.8rem; + } + } + + .file-input-group { + .file-label { + padding: 0.625rem 0.875rem; + font-size: 0.9rem; + } + } + + .file-selected { + flex-direction: column; + align-items: center; + gap: 0.75rem; + text-align: center; + + .image-preview-container { + .image-preview { + width: 50px; + height: 50px; + } + } + + .file-info { + flex-direction: column; + gap: 0.5rem; + width: 100%; + + .file-name { + font-size: 0.8rem; + } + } + } + } +} diff --git a/client/src/app/components/registration-modal/registration-modal.component.spec.ts b/client/src/app/components/registration-modal/registration-modal.component.spec.ts new file mode 100644 index 000000000..d20e413b5 --- /dev/null +++ b/client/src/app/components/registration-modal/registration-modal.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; + +import { RegistrationModalComponent } from './registration-modal.component'; + +describe('RegistrationModalComponent', () => { + let component: RegistrationModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [provideNoopAnimations(), ], + imports: [RegistrationModalComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(RegistrationModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/components/registration-modal/registration-modal.component.ts b/client/src/app/components/registration-modal/registration-modal.component.ts new file mode 100644 index 000000000..0134223eb --- /dev/null +++ b/client/src/app/components/registration-modal/registration-modal.component.ts @@ -0,0 +1,500 @@ +import { Component, OnDestroy, inject, signal, computed, input, output, viewChild, ChangeDetectionStrategy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { + ButtonDirective, + FormModule, + GridModule, + ModalBodyComponent, + ModalComponent, + ModalHeaderComponent, + SpinnerComponent, + AlertComponent +} from '@coreui/angular'; +import { AuthService } from '../../service/auth.service'; +import type { User, UpdateUserInfoRequest } from '@f123dashboard/shared'; + +@Component({ + selector: 'app-registration-modal', + templateUrl: './registration-modal.component.html', + styleUrls: ['./registration-modal.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + FormsModule, + FormModule, + ButtonDirective, + GridModule, + ModalComponent, + ModalHeaderComponent, + ModalBodyComponent, + SpinnerComponent, + AlertComponent + ] +}) +export class RegistrationModalComponent implements OnDestroy { + private authService = inject(AuthService); + + // Modal mode - 'register' for new users, 'update' for editing profile + mode = signal<'register' | 'update'>('register'); + userData = signal(null); + + // Flag to track if we're completing email for existing user + isEmailCompletion = signal(false); + + // Registration form fields + name = signal(''); + surname = signal(''); + regUsername = signal(''); + regPassword = signal(''); + confirmPassword = signal(''); + email = signal(''); + selectedFile = signal(null); + imagePreviewUrl = signal(''); + // Store the processed base64 data + processedImageBase64 = signal(''); + + // State management + isLoading = signal(false); + singInErrorMessage = signal(''); + successMessage = signal(''); + + modal = viewChild.required('verticallyCenteredModal'); + + //property to control visibility + visible = signal(false); + + registrationSuccess = output(); + updateSuccess = output(); + + // Grid gutter configuration (readonly to prevent recreation on change detection) + readonly gutterConfig = { gy: 3 }; + + // Computed property for modal title + modalTitle = computed(() => { + if (this.mode() === 'register') { + return 'Crea il tuo account'; + } else if (this.isEmailCompletion()) { + return 'Completa il tuo profilo'; + } else { + return 'Modifica il tuo profilo'; + } + }); + + ngOnDestroy() { + // Clean up any preview URL and processed data + this.imagePreviewUrl.set(''); + this.processedImageBase64.set(''); + } + + // New method to open modal in update mode + openForUpdate(user: User) { + this.mode.set('update'); + this.userData.set(user); + this.isEmailCompletion.set(false); + this.singInErrorMessage.set(''); + this.successMessage.set(''); + this.populateUserData(user); + this.visible.set(true); + } + + // New method to open modal in register mode + openForRegistration() { + this.mode.set('register'); + this.userData.set(null); + this.isEmailCompletion.set(false); + this.clearForm(); + this.singInErrorMessage.set(''); + this.successMessage.set(''); + this.visible.set(true); + } + + // New method to open modal for email completion + openForEmailCompletion(user: User) { + this.mode.set('update'); + this.userData.set(user); + this.isEmailCompletion.set(true); + this.singInErrorMessage.set(''); + this.successMessage.set(''); + this.populateUserDataForEmailCompletion(user); + this.visible.set(true); + } + + private populateUserData(user: User) { + this.name.set(user.name || ''); + this.surname.set(user.surname || ''); + this.regUsername.set(user.username || ''); + this.email.set(user.mail || ''); + + // Set current user image if available + if (user.image) { + this.imagePreviewUrl.set(`data:image/jpeg;base64,${user.image}`); + this.processedImageBase64.set(user.image); + } else { + this.imagePreviewUrl.set(''); + this.processedImageBase64.set(''); + } + + // Clear password fields for update mode + this.regPassword.set(''); + this.confirmPassword.set(''); + this.selectedFile.set(null); + } + + private populateUserDataForEmailCompletion(user: User) { + this.name.set(user.name || ''); + this.surname.set(user.surname || ''); + this.regUsername.set(user.username || ''); + this.email.set(''); // Force email to be empty so user must fill it + + // Set current user image if available + if (user.image) { + this.imagePreviewUrl.set(`data:image/jpeg;base64,${user.image}`); + this.processedImageBase64.set(user.image); + } else { + this.imagePreviewUrl.set(''); + this.processedImageBase64.set(''); + } + + // Clear password fields for update mode + this.regPassword.set(''); + this.confirmPassword.set(''); + this.selectedFile.set(null); + } + + private clearForm() { + this.name.set(''); + this.surname.set(''); + this.regUsername.set(''); + this.regPassword.set(''); + this.confirmPassword.set(''); + this.email.set(''); + this.selectedFile.set(null); + this.imagePreviewUrl.set(''); + this.processedImageBase64.set(''); + this.singInErrorMessage.set(''); + this.successMessage.set(''); + this.isEmailCompletion.set(false); + this.userData.set(null); + } + + public close(): void { + this.visible.set(false); + this.clearForm(); + } + + // Add this method to handle two-way binding + handleVisibilityChange(event: boolean) { + this.visible.set(event); + } + + + async onFileSelected(event: Event) { + const input = event.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + const file = input.files[0]; + + // Validate file type - only JPEG allowed + const allowedTypes = ['image/jpeg', 'image/jpg']; + if (!allowedTypes.includes(file.type)) { + this.singInErrorMessage.set('Selezionare solo file immagine JPEG'); + this.selectedFile.set(null); + // Reset the input + input.value = ''; + return; + } + + // Validate file size (max 5MB) + const maxSize = 5 * 1024 * 1024; // 5MB in bytes + if (file.size > maxSize) { + this.singInErrorMessage.set('L\'immagine deve essere inferiore a 5MB'); + this.selectedFile.set(null); + // Reset the input + input.value = ''; + return; + } + + this.selectedFile.set(file); + this.singInErrorMessage.set(''); // Clear any previous error + + try { + // Convert and resize the image, then create preview from the processed result + const processedImageBase64 = await this.convertFileToBase64(file); + + // Store the processed base64 data for later use + this.processedImageBase64.set(processedImageBase64); + + // Create preview URL from the processed base64 data + this.imagePreviewUrl.set(`data:image/jpeg;base64,${processedImageBase64}`); + } catch (error) { + console.error('Error processing image:', error); + this.singInErrorMessage.set('Errore durante l\'elaborazione dell\'immagine'); + this.selectedFile.set(null); + // Reset the input + input.value = ''; + } + } + } + + removeSelectedFile() { + // Clean up the previous preview URL - no need to revoke since it's now a data URL + this.imagePreviewUrl.set(''); + this.processedImageBase64.set(''); + + this.selectedFile.set(null); + // Reset the file input + const fileInput = document.getElementById('profileImage') as HTMLInputElement; + if (fileInput) { + fileInput.value = ''; + } + } + + async onRegistration() { + if (this.mode() === 'register') { + await this.handleRegistration(); + } else { + await this.handleUpdate(); + } + } + + private async handleRegistration() { + if (!this.validateRegistrationForm()) { + return; + } + + this.isLoading.set(true); + this.singInErrorMessage.set(''); + + try { + // Use the already processed base64 data if available + const imageData: string = this.processedImageBase64(); + + const response = await this.authService.register({ + username: this.regUsername(), + name: this.name(), + surname: this.surname(), + password: this.regPassword(), + mail: this.email(), + image: imageData + }); + + if (response.success) { + this.successMessage.set('Registrazione completata con successo!'); + this.registrationSuccess.emit(); + setTimeout(() => this.visible.set(false), 2000); + } else { + this.singInErrorMessage.set(response.message || 'Registrazione fallita. Riprova.'); + } + } catch (error) { + this.singInErrorMessage.set('Si è verificato un errore durante la registrazione. Riprova.'); + console.error('Registration error:', error); + } finally { + this.isLoading.set(false); + } + } + + private async handleUpdate() { + if (!this.validateUpdateForm()) { + return; + } + + this.isLoading.set(true); + this.singInErrorMessage.set(''); + + try { + // Prepare update data - only include changed fields + const updateData: UpdateUserInfoRequest = {}; + const currentUserData = this.userData(); + + if (currentUserData) { + // Only include fields that have changed + if (this.name() !== currentUserData.name) { + updateData.name = this.name(); + } + if (this.surname() !== currentUserData.surname) { + updateData.surname = this.surname(); + } + if (this.email() !== currentUserData.mail) { + updateData.mail = this.email(); + } + // Include image if a new one was selected or if existing image was removed + if (this.selectedFile() || (this.processedImageBase64() !== currentUserData.image)) { + updateData.image = this.processedImageBase64(); + } + } + + // Check if there are any changes + if (Object.keys(updateData).length === 0) { + this.singInErrorMessage.set('Nessuna modifica da salvare'); + this.isLoading.set(false); + return; + } + + const response = await this.authService.updateUserInfo(updateData as UpdateUserInfoRequest); + + if (response.success) { + this.successMessage.set('Profilo aggiornato con successo!'); + this.updateSuccess.emit(); + setTimeout(() => this.visible.set(false), 2000); + } else { + this.singInErrorMessage.set(response.message || 'Aggiornamento fallito. Riprova.'); + } + } catch (error) { + this.singInErrorMessage.set('Si è verificato un errore durante l\'aggiornamento. Riprova.'); + console.error('Update error:', error); + } finally { + this.isLoading.set(false); + } + } + + private convertFileToBase64(file: File): Promise { + return new Promise((resolve, reject) => { + // Crea un elemento immagine per ridimensionare l'immagine + const img = new Image(); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + if (!ctx) { + reject(new Error('Impossibile ottenere il contesto del canvas')); + return; + } + + img.onload = () => { + // Imposta le dimensioni del canvas a 80x80px + canvas.width = 80; + canvas.height = 80; + + // Calcola le dimensioni di ritaglio per mantenere le proporzioni + const { sourceX, sourceY, sourceWidth, sourceHeight } = this.calculateCropDimensions(img.width, img.height); + + // Disegna l'immagine ritagliata sul canvas + ctx.drawImage( + img, + sourceX, sourceY, sourceWidth, sourceHeight, // Rettangolo sorgente (area di ritaglio) + 0, 0, 80, 80 // Rettangolo destinazione (canvas) + ); + + // Converti il canvas in base64 + const base64String = canvas.toDataURL('image/jpeg', 0.8); // Qualità 0.8 per la compressione JPEG + // Rimuovi il prefisso dell'URL dei dati (es. "data:image/jpeg;base64,") + const base64Data = base64String.split(',')[1]; + resolve(base64Data); + + // Pulisci l'URL dell'oggetto + URL.revokeObjectURL(objectUrl); + }; + + img.onerror = () => { + URL.revokeObjectURL(objectUrl); + reject(new Error('Errore durante il caricamento dell\'immagine per il ridimensionamento')); + }; + + // Crea l'URL dell'oggetto per il file e impostalo come sorgente dell'immagine + const objectUrl = URL.createObjectURL(file); + img.src = objectUrl; + }); + } + + private calculateCropDimensions(imageWidth: number, imageHeight: number) { + // Calcola la dimensione dell'area di ritaglio quadrata (usa la dimensione più piccola) + const cropSize = Math.min(imageWidth, imageHeight); + + // Calcola la posizione per centrare il ritaglio + const sourceX = (imageWidth - cropSize) / 2; + const sourceY = (imageHeight - cropSize) / 2; + + return { + sourceX, + sourceY, + sourceWidth: cropSize, + sourceHeight: cropSize + }; + } + + private validateRegistrationForm(): boolean { + this.singInErrorMessage.set(''); + + if (!this.name() || !this.surname() || !this.regUsername() || !this.regPassword() || !this.email()) { + this.singInErrorMessage.set('Tutti i campi sono obbligatori'); + return false; + } + + if (this.regUsername().length < 3) { + this.singInErrorMessage.set('L\'username deve contenere almeno 3 caratteri'); + return false; + } + + if (this.regPassword().length < 8) { + this.singInErrorMessage.set('La password deve contenere almeno 8 caratteri'); + return false; + } + + if (this.regPassword() !== this.confirmPassword()) { + this.singInErrorMessage.set('Le password non corrispondono'); + return false; + } + + // Email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(this.email())) { + this.singInErrorMessage.set('Inserire un indirizzo email valido'); + return false; + } + + // Password strength validation + if (!this.authService.isPasswordStrong(this.regPassword())) { + this.singInErrorMessage.set('La password deve contenere almeno una lettera maiuscola, una lettera minuscola e un numero'); + return false; + } + + return true; + } + + private validateUpdateForm(): boolean { + this.singInErrorMessage.set(''); + + // In email completion mode, email and image are required + if (this.isEmailCompletion()) { + if (!this.email() || this.email().trim() === '') { + this.singInErrorMessage.set('Email è obbligatoria per completare il profilo'); + return false; + } + + // Email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(this.email())) { + this.singInErrorMessage.set('Inserire un indirizzo email valido'); + return false; + } + + // Image validation + if (!this.processedImageBase64() || this.processedImageBase64().trim() === '') { + this.singInErrorMessage.set('L\'immagine del profilo è obbligatoria'); + return false; + } + + return true; + } + + // In normal update mode, all fields including image are required + if (!this.name() || !this.surname() || !this.email()) { + this.singInErrorMessage.set('Nome, cognome ed email sono obbligatori'); + return false; + } + + // Email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(this.email())) { + this.singInErrorMessage.set('Inserire un indirizzo email valido'); + return false; + } + + // Image validation + if (!this.processedImageBase64() || this.processedImageBase64().trim() === '') { + this.singInErrorMessage.set('L\'immagine del profilo è obbligatoria'); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/client/src/app/components/vote-history-table/vote-history-table.component.html b/client/src/app/components/vote-history-table/vote-history-table.component.html new file mode 100644 index 000000000..c41113719 --- /dev/null +++ b/client/src/app/components/vote-history-table/vote-history-table.component.html @@ -0,0 +1,167 @@ +
+ + + + + + + + + + + + + + @for (voto of getVoteArray(); track i; let i = $index) { + @let pilota = getPilota(voto); + @if (pilota !== null || i + 1 > 10) { + + + + + + + + + } + } + +
+ Voto + # + + + + Username + Nome + + Posizione di arrivo + Pos. + + Punti fatti + Pt. + + Risultato + +
+
+ @if (i + 1 <= 8) { +
{{ i + 1 }}
+ } @else if (i + 1 == 9) { +
GV
+ } @else if (i + 1 == 10) { +
DNF
+ } @else { +
TEAM
+ } +
+
+ @if (i + 1 <= 10) { + + } @else { + + } + + @if (i + 1 <= 10) { +
{{ pilota?.driver_username }}
+ } @else { +
{{ getConstructor(voto) || '-' }}
+ } +
+ @if (i + 1 <= 8) { +
+ {{ getPosizioneArrivo(voto) }} +
+ } @else if (i + 1 == 9) { +
+ @if (+getFastLap() === +voto) { + SI + } @else { + NO + } +
+ } @else if (i + 1 == 10) { +
+ @if (fantaService.isDnfCorrect(getDnf(), voto)) { + SI + } @else { + NO + } +
+ } @else { +
+ @if (fantaService.getWinningConstructorsForTrack(trackId()).includes(+voto)) { + SI + } @else { + NO + } +
+ } +
+
+ @if (i + 1 <= 8) { + {{ getPunti(voto) }} + } @else if (i + 1 == 9) { + {{ getPuntiFastLap() }} + } @else if (i + 1 == 10) { + {{ getPuntiDnf() }} + } @else { + {{ getPuntiConstructor() }} + } +
+
+
+ @if (i + 1 <= 8) { + + } @else if (i + 1 == 9) { + + } @else if (i + 1 == 10) { + + } @else { + + } +
+
+
+ +@if (showTotalPoints()) { +
+

+ Totale Punti: {{ getTotalPoints() }} +

+
+} diff --git a/client/src/app/components/vote-history-table/vote-history-table.component.scss b/client/src/app/components/vote-history-table/vote-history-table.component.scss new file mode 100644 index 000000000..cae729bd2 --- /dev/null +++ b/client/src/app/components/vote-history-table/vote-history-table.component.scss @@ -0,0 +1,232 @@ +// Vote History Table Styles +.table-responsive-custom { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.storico-table { + width: 100%; + font-size: 0.9rem; + + th, td { + padding: 0.5rem; + vertical-align: middle; + } + + th { + font-size: 0.85rem; + font-weight: 600; + white-space: nowrap; + } + + .col-voto { + width: 60px; + min-width: 50px; + } + + .col-avatar { + width: 70px; + min-width: 60px; + } + + .col-username { + min-width: 100px; + } + + .col-posizione { + width: 140px; + min-width: 80px; + } + + .col-punti { + width: 100px; + min-width: 70px; + } + + .col-risultato { + width: 100px; + min-width: 70px; + } + + .voto-label { + font-size: 0.9rem; + } + + .username-text { + font-size: 0.9rem; + } + + .posizione-value, + .punti-value { + font-size: 0.9rem; + } + + .risultato-icon svg { + width: 20px; + height: 20px; + } +} + +// Large screen table optimization +@media (min-width: 768px) { + .table-responsive-custom { + max-width: 900px; + margin-left: auto; + margin-right: auto; + } + + .storico-table { + th, td { + padding: 0.75rem; + } + } +} + +@media (min-width: 992px) { + .table-responsive-custom { + max-width: 1000px; + } +} + +@media (min-width: 1200px) { + .table-responsive-custom { + max-width: 1100px; + } +} + +@media (max-width: 650px) { + // Mobile table styles + .storico-table { + font-size: 0.75rem; + + th, td { + padding: 0.35rem 0.25rem; + } + + th { + font-size: 0.7rem; + } + + .col-voto { + width: 40px; + min-width: 35px; + } + + .col-avatar { + width: 50px; + min-width: 45px; + } + + .col-username { + min-width: 70px; + max-width: 100px; + } + + .col-posizione { + width: 55px; + min-width: 50px; + } + + .col-punti { + width: 50px; + min-width: 45px; + } + + .col-risultato { + width: 50px; + min-width: 45px; + } + + .voto-label { + font-size: 0.75rem; + } + + .username-text { + font-size: 0.75rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .posizione-value, + .punti-value { + font-size: 0.75rem; + } + + .risultato-icon svg { + width: 16px; + height: 16px; + } + + .storico-avatar { + --cui-avatar-size: 2rem; + } + } +} + +@media (max-width: 400px) { + // Extra small screens - ultra compact table + .storico-table { + font-size: 0.7rem; + + th, td { + padding: 0.25rem 0.15rem; + } + + th { + font-size: 0.65rem; + } + + .col-voto { + width: 35px; + min-width: 30px; + } + + .col-avatar { + width: 45px; + min-width: 40px; + } + + .col-username { + min-width: 60px; + max-width: 80px; + } + + .col-posizione { + width: 45px; + min-width: 40px; + } + + .col-punti { + width: 40px; + min-width: 35px; + } + + .col-risultato { + width: 40px; + min-width: 35px; + } + + .voto-label { + font-size: 0.7rem; + } + + .username-text { + font-size: 0.7rem; + } + + .posizione-value, + .punti-value { + font-size: 0.7rem; + } + + .risultato-icon svg { + width: 14px; + height: 14px; + } + + .storico-avatar { + --cui-avatar-size: 1.75rem; + } + } +} diff --git a/client/src/app/components/vote-history-table/vote-history-table.component.spec.ts b/client/src/app/components/vote-history-table/vote-history-table.component.spec.ts new file mode 100644 index 000000000..3945dfb0d --- /dev/null +++ b/client/src/app/components/vote-history-table/vote-history-table.component.spec.ts @@ -0,0 +1,526 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { VoteHistoryTableComponent } from './vote-history-table.component'; +import { FantaService } from '../../service/fanta.service'; +import { DbDataService } from '../../service/db-data.service'; +import type { FantaVote, DriverData, Constructor, RaceResult } from '@f123dashboard/shared'; +import { cilPeople, cilCheckAlt, cilX, cilSwapVertical } from '@coreui/icons'; +import { signal, WritableSignal, computed } from '@angular/core'; + +describe('VoteHistoryTableComponent', () => { + let component: VoteHistoryTableComponent; + let fixture: ComponentFixture; + let mockFantaService: jasmine.SpyObj; + let mockDbDataService: Partial; + let allDriversSignal: WritableSignal; + let constructorsSignal: WritableSignal; + + const mockDrivers: DriverData[] = [ + { + driver_id: 1, + driver_username: 'driver1', + driver_name: 'John', + driver_surname: 'Doe', + driver_description: '', + driver_license_pt: 3, + driver_consistency_pt: 0, + driver_fast_lap_pt: 0, + drivers_dangerous_pt: 0, + driver_ingenuity_pt: 0, + driver_strategy_pt: 0, + driver_color: '#FF0000', + car_name: 'RedBull', + car_overall_score: 90, + total_sprint_points: 0, + total_free_practice_points: 0, + total_qualifying_points: 0, + total_full_race_points: 0, + total_race_points: 0, + total_points: 0 + }, + { + driver_id: 2, + driver_username: 'driver2', + driver_name: 'Jane', + driver_surname: 'Smith', + driver_description: '', + driver_license_pt: 3, + driver_consistency_pt: 0, + driver_fast_lap_pt: 0, + drivers_dangerous_pt: 0, + driver_ingenuity_pt: 0, + driver_strategy_pt: 0, + driver_color: '#00FF00', + car_name: 'Mercedes', + car_overall_score: 85, + total_sprint_points: 0, + total_free_practice_points: 0, + total_qualifying_points: 0, + total_full_race_points: 0, + total_race_points: 0, + total_points: 0 + } + ]; + + const mockConstructors: Constructor[] = [ + { + constructor_id: 1, + constructor_name: 'RedBull Racing', + constructor_color: '#FF0000', + driver_1_id: 1, + driver_1_username: 'driver1', + driver_1_tot_points: 0, + driver_2_id: 2, + driver_2_username: 'driver2', + driver_2_tot_points: 0, + constructor_tot_points: 0 + } + ]; + + const mockFantaVote: FantaVote = { + fanta_player_id: 1, + username: 'testuser', + track_id: 1, + id_1_place: 1, + id_2_place: 2, + id_3_place: 3, + id_4_place: 4, + id_5_place: 5, + id_6_place: 6, + id_7_place: 7, + id_8_place: 8, + id_fast_lap: 1, + id_dnf: 2, + season_id: 1, + constructor_id: 1 + }; + + const mockRaceResult: RaceResult = { + id: 1, + track_id: 1, + id_1_place: 1, + id_2_place: 2, + id_3_place: 3, + id_4_place: 4, + id_5_place: 5, + id_6_place: 6, + id_7_place: 7, + id_8_place: 8, + id_fast_lap: 1, + list_dnf: '2,3' + }; + + beforeEach(async () => { + mockFantaService = jasmine.createSpyObj('FantaService', [ + 'getRaceResult', + 'pointsWithAbsoluteDifference', + 'getFantaRacePoints', + 'isDnfCorrect', + 'getWinningConstructorsForTrack' + ]); + + allDriversSignal = signal(mockDrivers); + constructorsSignal = signal(mockConstructors); + mockDbDataService = { + allDrivers: allDriversSignal.asReadonly(), + constructors: computed(() => constructorsSignal()) + }; + + // Setup mocks that return different values based on trackId + mockFantaService.getRaceResult.and.callFake((trackId: number) => { + return trackId === 1 ? mockRaceResult : undefined; + }); + mockFantaService.getWinningConstructorsForTrack.and.callFake((trackId: number) => { + return trackId === 1 ? [1] : []; + }); + + mockFantaService.pointsWithAbsoluteDifference.and.returnValue(10); + mockFantaService.getFantaRacePoints.and.returnValue(100); + mockFantaService.isDnfCorrect.and.returnValue(true); + + await TestBed.configureTestingModule({ + providers: [ + provideNoopAnimations(), + { provide: FantaService, useValue: mockFantaService }, + { provide: DbDataService, useValue: mockDbDataService as DbDataService } + ], + imports: [VoteHistoryTableComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(VoteHistoryTableComponent); + component = fixture.componentInstance; + + // Set signal inputs using ComponentRef.setInput() + fixture.componentRef.setInput('fantaVote', { ...mockFantaVote }); + fixture.componentRef.setInput('trackId', 1); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('icon properties', () => { + it('should have cilPeople icon', () => { + expect(component.cilPeople).toEqual(cilPeople); + }); + + it('should have cilCheckAlt icon', () => { + expect(component.cilCheckAlt).toEqual(cilCheckAlt); + }); + + it('should have cilX icon', () => { + expect(component.cilX).toEqual(cilX); + }); + + it('should have cilSwapVertical icon', () => { + expect(component.cilSwapVertical).toEqual(cilSwapVertical); + }); + }); + + describe('getVoteArray', () => { + it('should return array of vote positions', () => { + const result = component.getVoteArray(); + expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 1]); + }); + + it('should return empty array when fantaVote is null', () => { + // Create a new fixture where the signal starts with a different value + const emptyFixture = TestBed.createComponent(VoteHistoryTableComponent); + emptyFixture.componentRef.setInput('fantaVote', { ...mockFantaVote }); + emptyFixture.componentRef.setInput('trackId', 1); + // Don't call detectChanges() to avoid triggering template evaluation + const result = emptyFixture.componentInstance.getVoteArray(); + expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 1]); + }); + }); + + describe('getPilota', () => { + it('should return driver data for valid id', () => { + const result = component.getPilota(1); + expect(result).toEqual(mockDrivers[0]); + }); + + it('should return null for invalid id', () => { + const result = component.getPilota(999); + expect(result).toBeNull(); + }); + }); + + describe('getConstructor', () => { + it('should return constructor name for valid id', () => { + const result = component.getConstructor(1); + expect(result).toBe('RedBull Racing'); + }); + + it('should return null for invalid id', () => { + const result = component.getConstructor(999); + expect(result).toBeNull(); + }); + }); + + describe('getPosizioneArrivo', () => { + it('should return position for driver who finished in race', () => { + const result = component.getPosizioneArrivo(1); + expect(result).toBe(1); + }); + + it('should return 0 if race result not found', () => { + const result = component.getPosizioneArrivo(999); + expect(result).toBe(0); + }); + + it('should return 0 if driver not found in results', () => { + const result = component.getPosizioneArrivo(999); + expect(result).toBe(0); + }); + }); + + describe('getVotoPos', () => { + it('should return voted position for driver', () => { + const result = component.getVotoPos(1); + expect(result).toBe(1); + }); + + it('should return 0 if driver not in vote', () => { + const result = component.getVotoPos(999); + expect(result).toBe(0); + }); + }); + + describe('getPunti', () => { + it('should return points for matching position', () => { + mockFantaService.pointsWithAbsoluteDifference.and.returnValue(10); + const result = component.getPunti(1); + expect(result).toBe(10); + expect(mockFantaService.pointsWithAbsoluteDifference).toHaveBeenCalledWith(1, 1); + }); + + it('should return 0 if position is 0', () => { + const result = component.getPunti(999); + expect(result).toBe(0); + }); + }); + + describe('getPuntiFastLap', () => { + it('should return points for correct fast lap prediction', () => { + const result = component.getPuntiFastLap(); + expect(result).toBe(5); + }); + + it('should return 0 for incorrect fast lap prediction', () => { + const updatedVote = { ...mockFantaVote, id_fast_lap: 999 }; + fixture.componentRef.setInput('fantaVote', updatedVote); + fixture.detectChanges(); + const result = component.getPuntiFastLap(); + expect(result).toBe(0); + }); + + it('should return 0 if no race result', () => { + // Create new fixture with trackId that returns no result + const noResultFixture = TestBed.createComponent(VoteHistoryTableComponent); + noResultFixture.componentRef.setInput('fantaVote', { ...mockFantaVote }); + noResultFixture.componentRef.setInput('trackId', 999); + noResultFixture.detectChanges(); + + const result = noResultFixture.componentInstance.getPuntiFastLap(); + expect(result).toBe(0); + }); + + it('should return 0 if fast lap id is 0', () => { + const updatedVote = { ...mockFantaVote, id_fast_lap: 0 }; + fixture.componentRef.setInput('fantaVote', updatedVote); + fixture.detectChanges(); + const result = component.getPuntiFastLap(); + expect(result).toBe(0); + }); + }); + + describe('getPuntiDnf', () => { + it('should return points for correct DNF prediction', () => { + mockFantaService.isDnfCorrect.and.returnValue(true); + const result = component.getPuntiDnf(); + expect(result).toBe(5); + }); + + it('should return 0 for incorrect DNF prediction', () => { + mockFantaService.isDnfCorrect.and.returnValue(false); + const result = component.getPuntiDnf(); + expect(result).toBe(0); + }); + + it('should return 0 if no race result', () => { + // Create new fixture with trackId that returns no result + const noResultFixture = TestBed.createComponent(VoteHistoryTableComponent); + noResultFixture.componentRef.setInput('fantaVote', { ...mockFantaVote }); + noResultFixture.componentRef.setInput('trackId', 999); + noResultFixture.detectChanges(); + + const result = noResultFixture.componentInstance.getPuntiDnf(); + expect(result).toBe(0); + }); + + it('should return 0 if dnf id is 0', () => { + const updatedVote = { ...mockFantaVote, id_dnf: 0 }; + fixture.componentRef.setInput('fantaVote', updatedVote); + fixture.detectChanges(); + const result = component.getPuntiDnf(); + expect(result).toBe(0); + }); + }); + + describe('getPuntiConstructor', () => { + it('should return points for correct constructor prediction', () => { + mockFantaService.getWinningConstructorsForTrack.and.returnValue([1]); + const result = component.getPuntiConstructor(); + expect(result).toBe(5); + }); + + it('should return 0 for incorrect constructor prediction', () => { + const updatedVote = { ...mockFantaVote, constructor_id: 999 }; + fixture.componentRef.setInput('fantaVote', updatedVote); + fixture.detectChanges(); + const result = component.getPuntiConstructor(); + expect(result).toBe(0); + }); + + it('should return 0 if no winning constructors', () => { + // Create new fixture with trackId that has no winners + const noWinnersFixture = TestBed.createComponent(VoteHistoryTableComponent); + noWinnersFixture.componentRef.setInput('fantaVote', { ...mockFantaVote }); + noWinnersFixture.componentRef.setInput('trackId', 999); + noWinnersFixture.detectChanges(); + + const result = noWinnersFixture.componentInstance.getPuntiConstructor(); + expect(result).toBe(0); + }); + + it('should return 0 if constructor id is 0', () => { + const updatedVote = { ...mockFantaVote, constructor_id: 0 }; + fixture.componentRef.setInput('fantaVote', updatedVote); + fixture.detectChanges(); + const result = component.getPuntiConstructor(); + expect(result).toBe(0); + }); + }); + + describe('getTotalPoints', () => { + it('should return total fanta race points', () => { + mockFantaService.getFantaRacePoints.and.returnValue(100); + const result = component.getTotalPoints(); + expect(result).toBe(100); + expect(mockFantaService.getFantaRacePoints).toHaveBeenCalledWith(1, 1); + }); + }); + + describe('isCorrect', () => { + it('should return check icon for correct prediction', () => { + mockFantaService.pointsWithAbsoluteDifference.and.returnValue(7); // CORRECT_RESPONSE_POINTS[0] + const result = component.isCorrect(1); + expect(result.icon).toEqual(cilCheckAlt); + expect(result.color).toBe('green'); + }); + + it('should return swap icon for close prediction (off by 1)', () => { + mockFantaService.pointsWithAbsoluteDifference.and.returnValue(4); // CORRECT_RESPONSE_POINTS[1] + const result = component.isCorrect(1); + expect(result.icon).toEqual(cilSwapVertical); + expect(result.color).toBe('#FFA600'); + }); + + it('should return swap icon for close prediction (off by 2)', () => { + mockFantaService.pointsWithAbsoluteDifference.and.returnValue(2); // CORRECT_RESPONSE_POINTS[2] + const result = component.isCorrect(1); + expect(result.icon).toEqual(cilSwapVertical); + expect(result.color).toBe('#FFA600'); + }); + + it('should return X icon for incorrect prediction', () => { + mockFantaService.pointsWithAbsoluteDifference.and.returnValue(0); + const result = component.isCorrect(1); + expect(result.icon).toEqual(cilX); + expect(result.color).toBe('red'); + }); + + it('should return X icon when position is 0', () => { + const result = component.isCorrect(999); + expect(result.icon).toEqual(cilX); + expect(result.color).toBe('red'); + }); + }); + + describe('isCorrectFastLap', () => { + it('should return check icon for correct fast lap', () => { + const result = component.isCorrectFastLap(); + expect(result.color).toBe('green'); + }); + + it('should return X icon for incorrect fast lap', () => { + const updatedVote = { ...mockFantaVote, id_fast_lap: 999 }; + fixture.componentRef.setInput('fantaVote', updatedVote); + fixture.detectChanges(); + const result = component.isCorrectFastLap(); + expect(result.color).toBe('red'); + }); + + it('should return X icon if no race result', () => { + // Create new fixture with trackId that returns no result + const noResultFixture = TestBed.createComponent(VoteHistoryTableComponent); + noResultFixture.componentRef.setInput('fantaVote', { ...mockFantaVote }); + noResultFixture.componentRef.setInput('trackId', 999); + noResultFixture.detectChanges(); + + const result = noResultFixture.componentInstance.isCorrectFastLap(); + expect(result.color).toBe('red'); + }); + }); + + describe('isCorrectDnf', () => { + it('should return check icon for correct DNF', () => { + mockFantaService.isDnfCorrect.and.returnValue(true); + const result = component.isCorrectDnf(); + expect(result.color).toBe('green'); + }); + + it('should return X icon for incorrect DNF', () => { + mockFantaService.isDnfCorrect.and.returnValue(false); + const result = component.isCorrectDnf(); + expect(result.color).toBe('red'); + }); + + it('should return X icon if no race result', () => { + // Create new fixture with trackId that returns no result + const noResultFixture = TestBed.createComponent(VoteHistoryTableComponent); + noResultFixture.componentRef.setInput('fantaVote', { ...mockFantaVote }); + noResultFixture.componentRef.setInput('trackId', 999); + noResultFixture.detectChanges(); + + const result = noResultFixture.componentInstance.isCorrectDnf(); + expect(result.color).toBe('red'); + }); + }); + + describe('isCorrectConstructor', () => { + it('should return green for correct constructor', () => { + mockFantaService.getWinningConstructorsForTrack.and.returnValue([1]); + const result = component.isCorrectConstructor(); + expect(result.color).toBe('green'); + }); + + it('should return red for incorrect constructor', () => { + const updatedVote = { ...mockFantaVote, constructor_id: 999 }; + fixture.componentRef.setInput('fantaVote', updatedVote); + fixture.detectChanges(); + const result = component.isCorrectConstructor(); + expect(result.color).toBe('red'); + }); + + it('should return red if no winning constructors', () => { + // Create new fixture with trackId that has no winners + const noWinnersFixture = TestBed.createComponent(VoteHistoryTableComponent); + noWinnersFixture.componentRef.setInput('fantaVote', { ...mockFantaVote }); + noWinnersFixture.componentRef.setInput('trackId', 999); + noWinnersFixture.detectChanges(); + + const result = noWinnersFixture.componentInstance.isCorrectConstructor(); + expect(result.color).toBe('red'); + }); + }); + + describe('getFastLap', () => { + it('should return fast lap driver id', () => { + const result = component.getFastLap(); + expect(result).toBe(1); + }); + + it('should return 0 if no race result', () => { + // Create new fixture with trackId that returns no result + const noResultFixture = TestBed.createComponent(VoteHistoryTableComponent); + noResultFixture.componentRef.setInput('fantaVote', { ...mockFantaVote }); + noResultFixture.componentRef.setInput('trackId', 999); + noResultFixture.detectChanges(); + + const result = noResultFixture.componentInstance.getFastLap(); + expect(result).toBe(0); + }); + }); + + describe('getDnf', () => { + it('should return DNF list', () => { + const result = component.getDnf(); + expect(result).toBe('2,3'); + }); + + it('should return empty string if no race result', () => { + // Create new fixture with trackId that returns no result + const noResultFixture = TestBed.createComponent(VoteHistoryTableComponent); + noResultFixture.componentRef.setInput('fantaVote', { ...mockFantaVote }); + noResultFixture.componentRef.setInput('trackId', 999); + noResultFixture.detectChanges(); + + const result = noResultFixture.componentInstance.getDnf(); + expect(result).toBe(''); + }); + }); +}); diff --git a/client/src/app/components/vote-history-table/vote-history-table.component.ts b/client/src/app/components/vote-history-table/vote-history-table.component.ts new file mode 100644 index 000000000..9a62a618b --- /dev/null +++ b/client/src/app/components/vote-history-table/vote-history-table.component.ts @@ -0,0 +1,241 @@ +import { CommonModule } from '@angular/common'; +import { Component, input, inject, computed, ChangeDetectionStrategy } from '@angular/core'; +import { TableDirective, AvatarComponent } from '@coreui/angular'; +import { IconDirective } from '@coreui/icons-angular'; +import { cilPeople, cilCheckAlt, cilX, cilSwapVertical } from '@coreui/icons'; +import { FantaService } from '../../service/fanta.service'; +import { DbDataService } from '../../service/db-data.service'; +import type { FantaVote, DriverData } from '@f123dashboard/shared'; +import { VoteStatus } from '../../model/fanta'; + +@Component({ + selector: 'app-vote-history-table', + imports: [ + CommonModule, + TableDirective, + IconDirective, + AvatarComponent + ], + templateUrl: './vote-history-table.component.html', + styleUrl: './vote-history-table.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class VoteHistoryTableComponent { + fantaService = inject(FantaService); + private dbData = inject(DbDataService); + + // Inputs as signals + fantaVote = input.required(); + trackId = input.required(); + showTotalPoints = input(true); + + public cilPeople: string[] = cilPeople; + public cilCheckAlt: string[] = cilCheckAlt; + public cilX: string[] = cilX; + public cilSwapVertical: string[] = cilSwapVertical; + + // Vote status constants + private readonly VOTE_STATUS_CORRECT: VoteStatus = { icon: cilCheckAlt, color: 'green' }; + private readonly VOTE_STATUS_INCORRECT: VoteStatus = { icon: cilX, color: 'red' }; + private readonly VOTE_STATUS_PARTIAL: VoteStatus = { icon: cilSwapVertical, color: '#FFA600' }; + + // Computed signals for reactive state + voteArray = computed(() => { + const vote = this.fantaVote(); + if (!vote) { + return []; + } + + return [ + vote.id_1_place, + vote.id_2_place, + vote.id_3_place, + vote.id_4_place, + vote.id_5_place, + vote.id_6_place, + vote.id_7_place, + vote.id_8_place, + vote.id_fast_lap, + vote.id_dnf, + vote.constructor_id + ]; + }); + + raceResult = computed(() => + this.fantaService.getRaceResult(this.trackId()) + ); + + fastLap = computed(() => + this.raceResult()?.id_fast_lap || 0 + ); + + dnfList = computed(() => + this.raceResult()?.list_dnf || '' + ); + + winningConstructors = computed(() => + this.fantaService.getWinningConstructorsForTrack(this.trackId()) + ); + + totalPoints = computed(() => + this.fantaService.getFantaRacePoints(this.fantaVote().fanta_player_id, this.trackId()) + ); + + /** + * Get vote array from FantaVote object + */ + getVoteArray(): number[] { + return this.voteArray(); + } + + getPilota(id: number): DriverData | null { + return this.dbData.allDrivers().find(driver => +driver.driver_id === +id) || null; + } + + getConstructor(id: number): string | null { + const constructor = this.dbData.constructors().find(c => c.constructor_id === id); + return constructor?.constructor_name || null; + } + + getPosizioneArrivo(pilotaId: number): number { + const result = this.raceResult(); + if (!result) { + return 0; + } + + const positions = [ + result.id_1_place, result.id_2_place, result.id_3_place, result.id_4_place, + result.id_5_place, result.id_6_place, result.id_7_place, result.id_8_place + ]; + + const position = positions.findIndex(id => +id === +pilotaId); + return position >= 0 ? position + 1 : 0; + } + + getVotoPos(pilotaId: number): number { + const voteArray = this.voteArray(); + const position = voteArray.slice(0, 8).findIndex(id => +id === +pilotaId); + return position >= 0 ? position + 1 : 0; + } + + getPunti(pilotaId: number): number { + const posizioneReale = this.getPosizioneArrivo(pilotaId); + const posizioneVotata = this.getVotoPos(pilotaId); + + if (posizioneReale === 0 || posizioneVotata === 0) { + return 0; + } + + // Both positions are 1-based (1-8), pass as-is to match service method usage + return this.fantaService.pointsWithAbsoluteDifference(posizioneReale, posizioneVotata); + } + + getPuntiFastLap(): number { + const result = this.raceResult(); + if (!result) { + return 0; + } + + const vote = this.fantaVote(); + return +result.id_fast_lap === +vote.id_fast_lap && vote.id_fast_lap !== 0 + ? FantaService.CORRECT_RESPONSE_FAST_LAP_POINTS + : 0; + } + + getPuntiDnf(): number { + const result = this.raceResult(); + if (!result) { + return 0; + } + + const vote = this.fantaVote(); + return this.fantaService.isDnfCorrect(result.list_dnf, +vote.id_dnf) && +vote.id_dnf !== 0 + ? FantaService.CORRECT_RESPONSE_DNF_POINTS + : 0; + } + + getPuntiConstructor(): number { + const winningConstructorIds = this.winningConstructors(); + if (winningConstructorIds.length === 0) { + return 0; + } + + const vote = this.fantaVote(); + return winningConstructorIds.includes(+vote.constructor_id) && +vote.constructor_id !== 0 + ? FantaService.CORRECT_RESPONSE_TEAM + : 0; + } + + getTotalPoints(): number { + return this.totalPoints(); + } + + isCorrect(pilotaId: number): VoteStatus { + const posizioneReale = this.getPosizioneArrivo(pilotaId); + const posizioneVotata = this.getVotoPos(pilotaId); + + if (posizioneReale === 0 || posizioneVotata === 0) { + return this.VOTE_STATUS_INCORRECT; + } + + const punti = this.fantaService.pointsWithAbsoluteDifference(posizioneReale, posizioneVotata); + + if (punti === FantaService.CORRECT_RESPONSE_POINTS[0]) { + return this.VOTE_STATUS_CORRECT; + } + + if (punti === FantaService.CORRECT_RESPONSE_POINTS[1] || + punti === FantaService.CORRECT_RESPONSE_POINTS[2]) { + return this.VOTE_STATUS_PARTIAL; + } + + return this.VOTE_STATUS_INCORRECT; + } + + isCorrectFastLap(): VoteStatus { + const result = this.raceResult(); + if (!result) { + return this.VOTE_STATUS_INCORRECT; + } + + const vote = this.fantaVote(); + const isCorrect = +result.id_fast_lap === +vote.id_fast_lap && +vote.id_fast_lap !== 0; + return isCorrect + ? this.VOTE_STATUS_CORRECT + : this.VOTE_STATUS_INCORRECT; + } + + isCorrectDnf(): VoteStatus { + const result = this.raceResult(); + if (!result) { + return this.VOTE_STATUS_INCORRECT; + } + + const vote = this.fantaVote(); + const isCorrect = this.fantaService.isDnfCorrect(result.list_dnf, +vote.id_dnf) && +vote.id_dnf !== 0; + return isCorrect + ? this.VOTE_STATUS_CORRECT + : this.VOTE_STATUS_INCORRECT; + } + + isCorrectConstructor(): VoteStatus { + const winningConstructorIds = this.winningConstructors(); + if (winningConstructorIds.length === 0) { + return this.VOTE_STATUS_INCORRECT; + } + + const vote = this.fantaVote(); + const isCorrect = winningConstructorIds.includes(+vote.constructor_id) && +vote.constructor_id !== 0; + return isCorrect + ? this.VOTE_STATUS_CORRECT + : this.VOTE_STATUS_INCORRECT; + } + + getFastLap(): number { + return this.fastLap(); + } + + getDnf(): string { + return this.dnfList(); + } +} diff --git a/client/src/app/guard/admin.guard.spec.ts b/client/src/app/guard/admin.guard.spec.ts new file mode 100644 index 000000000..b706aa2d2 --- /dev/null +++ b/client/src/app/guard/admin.guard.spec.ts @@ -0,0 +1,20 @@ +import { TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { CanActivateFn } from '@angular/router'; + +import { adminGuard } from './admin.guard'; + +describe('adminGuard', () => { + const executeGuard: CanActivateFn = (...guardParameters) => + TestBed.runInInjectionContext(() => adminGuard(...guardParameters)); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [provideNoopAnimations(), ],}); + }); + + it('should be created', () => { + expect(executeGuard).toBeTruthy(); + }); +}); diff --git a/client/src/app/guard/admin.guard.ts b/client/src/app/guard/admin.guard.ts new file mode 100644 index 000000000..883f6f4bb --- /dev/null +++ b/client/src/app/guard/admin.guard.ts @@ -0,0 +1,26 @@ +import { CanActivateFn, Router } from '@angular/router'; +import { inject } from '@angular/core'; +import { AuthService } from '../service/auth.service'; + +export const adminGuard: CanActivateFn = (route, state) => { + const authService = inject(AuthService); + const router = inject(Router); + + const isAuthenticated = authService.isAuthenticated(); + const currentUser = authService.currentUser(); + + // Check if user is authenticated + if (!isAuthenticated) { + sessionStorage.setItem('returnUrl', state.url); + return router.createUrlTree(['/login']); + } + + // Check if user is admin + if (!currentUser?.isAdmin) { + // Redirect non-admin users to dashboard + return router.createUrlTree(['/dashboard']); + } + + // Allow access for admin users + return true; +}; diff --git a/client/src/app/guard/auth.guard.spec.ts b/client/src/app/guard/auth.guard.spec.ts new file mode 100644 index 000000000..361a4601b --- /dev/null +++ b/client/src/app/guard/auth.guard.spec.ts @@ -0,0 +1,20 @@ +import { TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { CanActivateFn } from '@angular/router'; + +import { authGuard } from './auth.guard'; + +describe('authGuard', () => { + const executeGuard: CanActivateFn = (...guardParameters) => + TestBed.runInInjectionContext(() => authGuard(...guardParameters)); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [provideNoopAnimations(), ],}); + }); + + it('should be created', () => { + expect(executeGuard).toBeTruthy(); + }); +}); diff --git a/client/src/app/guard/auth.guard.ts b/client/src/app/guard/auth.guard.ts new file mode 100644 index 000000000..8a4b48d82 --- /dev/null +++ b/client/src/app/guard/auth.guard.ts @@ -0,0 +1,33 @@ +import { CanActivateFn, Router } from '@angular/router'; +import { inject } from '@angular/core'; +import { AuthService } from '../service/auth.service'; + +export const authGuard: CanActivateFn = (route, state) => { + const authService = inject(AuthService); + const router = inject(Router); + + const isAuthenticated = authService.isAuthenticated(); + const currentUser = authService.currentUser(); + + if (!isAuthenticated) { + sessionStorage.setItem('returnUrl', state.url); + + if (state.url === '/fanta') { + return router.createUrlTree(['/fanta-dashboard']); + } else if (state.url === '/admin') { + return router.createUrlTree(['/dashboard']); + } + + return router.createUrlTree(['/login']); // default fallback + } else if (state.url === '/admin') { + if (currentUser?.isAdmin) { + return true; // allow access + } else { + return router.createUrlTree(['/dashboard']); // redirect if not admin + } + } + + return true; +}; + + diff --git a/src/app/icons/icon-subset.ts b/client/src/app/icons/icon-subset.ts similarity index 92% rename from src/app/icons/icon-subset.ts rename to client/src/app/icons/icon-subset.ts index fda755c0e..5d71f6fff 100644 --- a/src/app/icons/icon-subset.ts +++ b/client/src/app/icons/icon-subset.ts @@ -7,8 +7,10 @@ import { cibCcVisa, cibFacebook, cibGoogle, + cibInstagram, cibLinkedin, cibSkype, + cibTwitch, cibTwitter, cifBr, cifEs, @@ -49,6 +51,7 @@ import { cilFile, cilGrid, cilHome, + cilHappy, cilInbox, cilIndentDecrease, cilIndentIncrease, @@ -56,6 +59,7 @@ import { cilJustifyCenter, cilLanguage, cilLayers, + cilLink, cilList, cilListNumbered, cilLocationPin, @@ -73,6 +77,7 @@ import { cilPen, cilPencil, cilPeople, + cilChevronDoubleUp, cilPrint, cilPuzzle, cilReportSlash, @@ -83,6 +88,7 @@ import { cilShareBoxed, cilSpeech, cilSpeedometer, + cilGamepad, cilSpreadsheet, cilStar, cilSun, @@ -93,7 +99,8 @@ import { cilUser, cilUserFemale, cilUserFollow, - cilUserUnfollow + cilUserUnfollow, + cilWarning } from '@coreui/icons'; import { signet } from './signet'; @@ -108,8 +115,10 @@ export const iconSubset = { cibCcVisa, cibFacebook, cibGoogle, + cibInstagram, cibLinkedin, cibSkype, + cibTwitch, cibTwitter, cifBr, cifEs, @@ -150,6 +159,7 @@ export const iconSubset = { cilFile, cilGrid, cilHome, + cilHappy, cilInbox, cilIndentDecrease, cilIndentIncrease, @@ -157,6 +167,7 @@ export const iconSubset = { cilJustifyCenter, cilLanguage, cilLayers, + cilLink, cilList, cilListNumbered, cilLocationPin, @@ -174,6 +185,7 @@ export const iconSubset = { cilPen, cilPencil, cilPeople, + cilChevronDoubleUp, cilPrint, cilPuzzle, cilReportSlash, @@ -184,6 +196,7 @@ export const iconSubset = { cilShareBoxed, cilSpeech, cilSpeedometer, + cilGamepad, cilSpreadsheet, cilStar, cilSun, @@ -195,6 +208,7 @@ export const iconSubset = { cilUserFemale, cilUserFollow, cilUserUnfollow, + cilWarning, logo, signet }; @@ -208,8 +222,10 @@ export enum IconSubset { cibCcVisa = 'cibCcVisa', cibFacebook = 'cibFacebook', cibGoogle = 'cibGoogle', + cibInstagram = 'cibInstagram', cibLinkedin = 'cibLinkedin', cibSkype = 'cibSkype', + cibTwitch = 'cibTwitch', cibTwitter = 'cibTwitter', cifBr = 'cifBr', cifEs = 'cifEs', @@ -241,7 +257,7 @@ export enum IconSubset { cilCommentSquare = 'cilCommentSquare', cilContrast = 'cilContrastś', cilCreditCard = 'cilCreditCard', - cilCursor = 'cilCursor', + cilCursor = 'cilCursor', cilDescription = 'cilDescription', cilDollar = 'cilDollar', cilDrop = 'cilDrop', @@ -250,6 +266,7 @@ export enum IconSubset { cilFile = 'cilFile', cilGrid = 'cilGrid', cilHome = 'cilHome', + cilHappy = 'cilHappy', cilInbox = 'cilInbox', cilIndentDecrease = 'cilIndentDecrease', cilIndentIncrease = 'cilIndentIncrease', @@ -257,6 +274,7 @@ export enum IconSubset { cilJustifyCenter = 'cilJustifyCenter', cilLanguage = 'cilLanguage', cilLayers = 'cilLayers', + cilLink = 'cilLink', cilList = 'cilList', cilListNumbered = 'cilListNumbered', cilLocationPin = 'cilLocationPin', @@ -274,6 +292,7 @@ export enum IconSubset { cilPen = 'cilPen', cilPencil = 'cilPencil', cilPeople = 'cilPeople', + cilChevronDoubleUp = 'cilChevronDoubleUp', cilPrint = 'cilPrint', cilPuzzle = 'cilPuzzle', cilReportSlash = 'cilReportSlash', @@ -284,6 +303,7 @@ export enum IconSubset { cilShareBoxed = 'cilShareBoxed', cilSpeech = 'cilSpeech', cilSpeedometer = 'cilSpeedometer', + cilGamepad = 'cilGamepad', cilSpreadsheet = 'cilSpreadsheet', cilStar = 'cilStar', cilSun = 'cilSun', @@ -295,6 +315,7 @@ export enum IconSubset { cilUserFemale = 'cilUserFemale', cilUserFollow = 'cilUserFollow', cilUserUnfollow = 'cilUserUnfollow', + cilWarning = 'cilWarning', logo = 'logo', signet = 'signet' } diff --git a/src/app/icons/logo.ts b/client/src/app/icons/logo.ts similarity index 100% rename from src/app/icons/logo.ts rename to client/src/app/icons/logo.ts diff --git a/src/app/icons/signet.ts b/client/src/app/icons/signet.ts similarity index 100% rename from src/app/icons/signet.ts rename to client/src/app/icons/signet.ts diff --git a/client/src/app/layout/default-layout/_nav.ts b/client/src/app/layout/default-layout/_nav.ts new file mode 100644 index 000000000..3cf3e5f7d --- /dev/null +++ b/client/src/app/layout/default-layout/_nav.ts @@ -0,0 +1,93 @@ +import { INavData } from '@coreui/angular'; +import { cilCoffee } from '@coreui/icons'; +import { AuthService } from './../../service/auth.service'; + + +export const getNavItems = (isAdmin: boolean): INavData[] => { + const items: INavData[] = [ + { + name: 'Dashboard', + url: '/dashboard', + iconComponent: { name: 'cil-speedometer' }, + }, + { + name: 'Piloti', + url: '/piloti', + iconComponent: { name: 'cil-people' }, + }, + { + name: 'Regole', + url: '/regole', + iconComponent: { name: 'cil-description' }, + }, + { + name: 'Campionato', + url: '/championship', + iconComponent: { name: 'cil-calendar' }, + }, + { + name: 'Fanta', + url: '/fanta', + iconComponent: { name: 'cil-gamepad' } + }, + { + name: 'Albo D\'oro', + url: '/albo-d-oro', + iconComponent: { name: 'cil-star' }, + badge: { + color: 'success', + text: 'NEW' + } + }, + { + name: 'Crediti', + url: '/credits', + iconComponent: { name: 'cil-coffee' }, + badge: { + color: 'success', + text: 'NEW' + } + }, + { + name: 'Minigiochi', + url: '/playground', + iconComponent: { name: 'cil-happy' }, + badge: { + color: 'success', + text: 'NEW' + } + } + ]; + + // Add Admin only if the user is admin + if (isAdmin) { + items.push({ + name: 'Admin', + url: '/admin', + iconComponent: { name: 'cil-settings' }, + badge: { + color: 'success', + text: 'NEW' + }, + children: [ + { + name: 'Gestione Risultati', + url: '/admin/result-edit', + iconComponent: { name: 'cil-pencil' } + }, + { + name: 'Modifica GP', + url: '/admin/gp-edit', + iconComponent: { name: 'cil-calendar' } + }, + { + name: 'Gestione Utenti', + url: '/admin/change-password', + iconComponent: { name: 'cil-user' } + } + ] + }); + } + + return items; +}; \ No newline at end of file diff --git a/client/src/app/layout/default-layout/default-footer/default-footer.component.html b/client/src/app/layout/default-layout/default-footer/default-footer.component.html new file mode 100644 index 000000000..83dad74d9 --- /dev/null +++ b/client/src/app/layout/default-layout/default-footer/default-footer.component.html @@ -0,0 +1,7 @@ + +
+ © fakeFIA +
+
+
+ diff --git a/src/app/layout/default-layout/default-header/default-header.component.scss b/client/src/app/layout/default-layout/default-footer/default-footer.component.scss similarity index 100% rename from src/app/layout/default-layout/default-header/default-header.component.scss rename to client/src/app/layout/default-layout/default-footer/default-footer.component.scss diff --git a/src/app/layout/default-layout/default-footer/default-footer.component.spec.ts b/client/src/app/layout/default-layout/default-footer/default-footer.component.spec.ts similarity index 84% rename from src/app/layout/default-layout/default-footer/default-footer.component.spec.ts rename to client/src/app/layout/default-layout/default-footer/default-footer.component.spec.ts index baa929001..36d4d0e01 100644 --- a/src/app/layout/default-layout/default-footer/default-footer.component.spec.ts +++ b/client/src/app/layout/default-layout/default-footer/default-footer.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { DefaultFooterComponent } from './default-footer.component'; @@ -8,6 +9,7 @@ describe('DefaultFooterComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ + providers: [provideNoopAnimations(), ], imports: [DefaultFooterComponent] }) .compileComponents(); diff --git a/src/app/layout/default-layout/default-footer/default-footer.component.ts b/client/src/app/layout/default-layout/default-footer/default-footer.component.ts similarity index 80% rename from src/app/layout/default-layout/default-footer/default-footer.component.ts rename to client/src/app/layout/default-layout/default-footer/default-footer.component.ts index 8795dc035..5af0cf5c1 100644 --- a/src/app/layout/default-layout/default-footer/default-footer.component.ts +++ b/client/src/app/layout/default-layout/default-footer/default-footer.component.ts @@ -4,8 +4,7 @@ import { FooterComponent } from '@coreui/angular'; @Component({ selector: 'app-default-footer', templateUrl: './default-footer.component.html', - styleUrls: ['./default-footer.component.scss'], - standalone: true, + styleUrls: ['./default-footer.component.scss'] }) export class DefaultFooterComponent extends FooterComponent { constructor() { diff --git a/client/src/app/layout/default-layout/default-header/default-header.component.html b/client/src/app/layout/default-layout/default-header/default-header.component.html new file mode 100644 index 000000000..dbdece916 --- /dev/null +++ b/client/src/app/layout/default-layout/default-header/default-header.component.html @@ -0,0 +1,87 @@ + + + + + + + + +
+ +
+ + + + + + + + + + + + +
+ + +
+ +
+ + + + + + +
+ @for (mode of colorModes; track mode.name) { + + } +
+
+
\ No newline at end of file diff --git a/client/src/app/layout/default-layout/default-header/default-header.component.scss b/client/src/app/layout/default-layout/default-header/default-header.component.scss new file mode 100644 index 000000000..37c708461 --- /dev/null +++ b/client/src/app/layout/default-layout/default-header/default-header.component.scss @@ -0,0 +1,34 @@ +.clock { + display: flex; /* Imposta il layout flessibile */ + justify-content: center; /* Centra orizzontalmente */ + align-items: center; /* Centra verticalmente */ + margin-top: 10px; /* Mantiene il margine superiore */ + height: 100%; /* Se necessario, assicura che l'altezza del contenitore permetta la centratura verticale */ + } + +.external-link-button svg { + color: inherit; + transition: color 0.2s ease; +} + +.external-link-button:hover svg { + color: #28a745; +} + +.twitch-link, +.instagram-link { + color: inherit; + transition: color 0.2s ease; +} + +/* Twitch hover color */ +.twitch-link:hover { + color: #6441a5 !important; +} + +/* Instagram hover color */ +.instagram-link:hover { + color: #c13584 !important; +} + + diff --git a/src/app/layout/default-layout/default-header/default-header.component.spec.ts b/client/src/app/layout/default-layout/default-header/default-header.component.spec.ts similarity index 91% rename from src/app/layout/default-layout/default-header/default-header.component.spec.ts rename to client/src/app/layout/default-layout/default-header/default-header.component.spec.ts index b21f2f62b..f445413fd 100644 --- a/src/app/layout/default-layout/default-header/default-header.component.spec.ts +++ b/client/src/app/layout/default-layout/default-header/default-header.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { ReactiveFormsModule } from '@angular/forms'; import { RouterTestingModule } from '@angular/router/testing'; import { @@ -25,7 +26,7 @@ describe('DefaultHeaderComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [GridModule, HeaderModule, IconModule, NavModule, BadgeModule, AvatarModule, DropdownModule, BreadcrumbModule, RouterTestingModule, SidebarModule, ProgressModule, ButtonGroupModule, ReactiveFormsModule, DefaultHeaderComponent], - providers: [IconSetService] + providers: [provideNoopAnimations(), IconSetService] }) .compileComponents(); }); diff --git a/client/src/app/layout/default-layout/default-header/default-header.component.ts b/client/src/app/layout/default-layout/default-header/default-header.component.ts new file mode 100644 index 000000000..497605b64 --- /dev/null +++ b/client/src/app/layout/default-layout/default-header/default-header.component.ts @@ -0,0 +1,49 @@ +import { NgTemplateOutlet } from '@angular/common'; +import { Component, computed, inject, input } from '@angular/core'; +import { + ColorModeService, + ContainerComponent, + DropdownComponent, + DropdownItemDirective, + DropdownMenuDirective, + DropdownToggleDirective, + HeaderComponent, + HeaderNavComponent, + HeaderTogglerDirective, + SidebarToggleDirective +} from '@coreui/angular'; + +import { IconDirective } from '@coreui/icons-angular'; + +import { CountdownComponent } from '../../../components/countdown/countdown.component' +import { LoginComponent } from '../../../components/login/login.component' +import { icons } from "../../../../components/link-box/link-box.component"; + +@Component({ + selector: 'app-default-header', + templateUrl: './default-header.component.html', + imports: [LoginComponent, CountdownComponent, ContainerComponent, HeaderTogglerDirective, SidebarToggleDirective, IconDirective, HeaderNavComponent, NgTemplateOutlet, DropdownComponent, DropdownToggleDirective, DropdownMenuDirective, DropdownItemDirective] + }) +export class DefaultHeaderComponent extends HeaderComponent { + + readonly #colorModeService = inject(ColorModeService); + readonly colorMode = this.#colorModeService.colorMode; + readonly linkIcon = icons; + readonly colorModes = [ + { name: 'light', text: 'Light', icon: 'cilSun' }, + { name: 'dark', text: 'Dark', icon: 'cilMoon' }, + { name: 'auto', text: 'Auto', icon: 'cilContrast' } + ]; + + readonly icons = computed(() => { + const currentMode = this.colorMode(); + return this.colorModes.find(mode => mode.name === currentMode)?.icon ?? 'cilSun'; + }); + + constructor() { + super(); + } + + sidebarId = input('sidebar1'); + +} diff --git a/client/src/app/layout/default-layout/default-layout.component.html b/client/src/app/layout/default-layout/default-layout.component.html new file mode 100644 index 000000000..d149bdfd6 --- /dev/null +++ b/client/src/app/layout/default-layout/default-layout.component.html @@ -0,0 +1,71 @@ + + + + + + + RaceForFederica Logo + RaceForFederica + + + + RaceForFederica Logo + + + + + + + + + @if (!sidebar1.narrow) { + + + + } + + + +
+ + + +
+ + + + +
+ + +
diff --git a/src/app/layout/default-layout/default-layout.component.scss b/client/src/app/layout/default-layout/default-layout.component.scss similarity index 76% rename from src/app/layout/default-layout/default-layout.component.scss rename to client/src/app/layout/default-layout/default-layout.component.scss index e7f3a7857..b04ed199b 100644 --- a/src/app/layout/default-layout/default-layout.component.scss +++ b/client/src/app/layout/default-layout/default-layout.component.scss @@ -2,12 +2,19 @@ .ng-scrollbar { --scrollbar-padding: 1px; --scrollbar-size: 5px; - --scrollbar-thumb-color: var(--cui-gray-500, #999); - --scrollbar-thumb-hover-color: var(--cui-gray-400, #999); + --scrollbar-thumb-color: var(--cui-gray-500, #aab3c5); + --scrollbar-thumb-hover-color: var(--cui-gray-400, #cfd4de); --scrollbar-hover-size: calc(var(--scrollbar-size) * 1.5); + --scrollbar-border-radius: 5px; + } + .ng-scroll-content { + display: flex; + min-height: 100%; } } + + // ng-scrollbar css variables //.cui-scrollbar { // --scrollbar-border-radius: 7px; diff --git a/client/src/app/layout/default-layout/default-layout.component.ts b/client/src/app/layout/default-layout/default-layout.component.ts new file mode 100644 index 000000000..03935035a --- /dev/null +++ b/client/src/app/layout/default-layout/default-layout.component.ts @@ -0,0 +1,53 @@ +import { Component, inject, computed } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterLink, RouterOutlet } from '@angular/router'; +import { NgScrollbar } from 'ngx-scrollbar'; + +import { + ContainerComponent, + ShadowOnScrollDirective, + SidebarBrandComponent, + SidebarComponent, + SidebarFooterComponent, + SidebarHeaderComponent, + SidebarNavComponent, + SidebarToggleDirective, + SidebarTogglerDirective +} from '@coreui/angular'; + +import { DefaultFooterComponent, DefaultHeaderComponent } from './'; +import { getNavItems } from './_nav'; +import { LoadingSpinnerComponent } from '../../../components/loading-spinner/loading-spinner.component'; +import { AuthService } from './../../service/auth.service'; + +@Component({ + selector: 'app-dashboard', + templateUrl: './default-layout.component.html', + styleUrls: ['./default-layout.component.scss'], + imports: [ + CommonModule, + SidebarComponent, + SidebarHeaderComponent, + SidebarBrandComponent, + RouterLink, + NgScrollbar, + SidebarNavComponent, + SidebarFooterComponent, + SidebarToggleDirective, + SidebarTogglerDirective, + DefaultHeaderComponent, + ShadowOnScrollDirective, + ContainerComponent, + RouterOutlet, + DefaultFooterComponent, + LoadingSpinnerComponent + ] +}) +export class DefaultLayoutComponent { + private authService = inject(AuthService); + + // Use computed to reactively update navigation based on user + public navItems = computed(() => + getNavItems(this.authService.currentUser()?.isAdmin ?? false) + ); +} diff --git a/src/app/layout/default-layout/index.ts b/client/src/app/layout/default-layout/index.ts similarity index 100% rename from src/app/layout/default-layout/index.ts rename to client/src/app/layout/default-layout/index.ts diff --git a/src/app/layout/index.ts b/client/src/app/layout/index.ts similarity index 100% rename from src/app/layout/index.ts rename to client/src/app/layout/index.ts diff --git a/client/src/app/model/championship.ts b/client/src/app/model/championship.ts new file mode 100644 index 000000000..5d94b54bc --- /dev/null +++ b/client/src/app/model/championship.ts @@ -0,0 +1,11 @@ +export interface GpResult{ + trackId: number, + hasSprint: boolean, + raceResult: number[]; + raceDnfResult: number[]; + sprintResult: number[]; + sprintDnfResult: number[]; + qualiResult: number[]; + fpResult: number[]; + seasonId: number; + } \ No newline at end of file diff --git a/client/src/app/model/constants.ts b/client/src/app/model/constants.ts new file mode 100644 index 000000000..8de48fe26 --- /dev/null +++ b/client/src/app/model/constants.ts @@ -0,0 +1,45 @@ +import { cifAe, cifAt, cifAu, cifAz, cifBe, cifBh, cifBr, cifCa, cifCn, cifEs, cifGb, cifHu, cifIt, cifJp, cifMc, cifMx, cifNl, cifQa, cifSa, cifSg, cifUs } from '@coreui/icons'; + +export const medals = new Map([ + [1, "medal_first.svg"], + [2, "medal_second.svg"], + [3, "medal_third.svg"] +]); + +export const posizioni = new Map([ + [1, "Primo"], + [2, "Secondo"], + [3, "Terzo"], + [4, "Quarto"], + [5, "Quinto"], + [6, "Sesto"], + [7, "Settimo"], + [8, "Ottavo"], + [9, "Giro Veloce"], + [10, "DNF"], + [11, "Team Vincente"] +]); + +export const allFlags: Record = { + "Barhain": cifBh, + "Arabia Saudita": cifSa, + "Australia": cifAu, + "Giappone": cifJp, + "Cina": cifCn, + "USA": cifUs, + "Monaco": cifMc, + "Canada": cifCa, + "Spagna": cifEs, + "Austria": cifAt, + "UK": cifGb, + "Ungheria": cifHu, + "Belgio": cifBe, + "Olanda": cifNl, + "Italia": cifIt, + "Azerbaijan": cifAz, + "Singapore": cifSg, + "Messico": cifMx, + "Brasile": cifBr, + "Qatar": cifQa, + "Emirati Arabi Uniti": cifAe, +}; diff --git a/client/src/app/model/dashboard.model.ts b/client/src/app/model/dashboard.model.ts new file mode 100644 index 000000000..9c1420c26 --- /dev/null +++ b/client/src/app/model/dashboard.model.ts @@ -0,0 +1,13 @@ +export interface DriverOfWeek { + driver_username: string; + driver_id: number; + points: number; +} + +export interface ConstructorOfWeek { + constructor_name: string; + constructor_id: number; + constructor_driver_1_id: number; + constructor_driver_2_id: number; + points: number; +} diff --git a/client/src/app/model/fanta.ts b/client/src/app/model/fanta.ts new file mode 100644 index 000000000..cbaf12ba0 --- /dev/null +++ b/client/src/app/model/fanta.ts @@ -0,0 +1,106 @@ +import type { FantaVote } from '@f123dashboard/shared'; + +export interface FantaPlayer { + username: string, + name: string, + surname: string, + password: string, + image: File +} + +/** + * Represents the visual status of a vote (correct, partial, incorrect). + */ +export interface VoteStatus { + icon: string[]; + color: string; +} +/** + * Vote index constants for accessing vote array positions. + * Maps human-readable names to array indices (0-based). + */ +export const VOTE_INDEX = { + PLACE_1: 0, + PLACE_2: 1, + PLACE_3: 2, + PLACE_4: 3, + PLACE_5: 4, + PLACE_6: 5, + PLACE_7: 6, + PLACE_8: 7, + FAST_LAP: 8, + DNF: 9, + CONSTRUCTOR: 10 +} as const; + +/** + * Form status constants for vote submission state. + */ +export const FORM_STATUS = { + SUCCESS: 1, + VALIDATION_ERROR: 2, + SAVE_ERROR: 3 +} as const; + +/** + * Number of driver positions in a vote. + */ +export const DRIVER_POSITIONS_COUNT = 8; + +/** + * Total number of vote fields. + */ +export const TOTAL_VOTE_FIELDS = 11; + +/** + * Helper class to convert between Fanta interface and vote array format. + */ +export class FantaVoteHelper { + /** + * Converts a Fanta object to a vote array. + */ + static toArray(fanta: FantaVote): readonly number[] { + return [ + fanta.id_1_place, + fanta.id_2_place, + fanta.id_3_place, + fanta.id_4_place, + fanta.id_5_place, + fanta.id_6_place, + fanta.id_7_place, + fanta.id_8_place, + fanta.id_fast_lap, + fanta.id_dnf, + fanta.constructor_id + ] as const; + } + + /** + * Converts a vote array to a partial Fanta object (without user/track info). + */ + static fromArray(votes: number[]): Pick { + return { + id_1_place: votes[VOTE_INDEX.PLACE_1] || 0, + id_2_place: votes[VOTE_INDEX.PLACE_2] || 0, + id_3_place: votes[VOTE_INDEX.PLACE_3] || 0, + id_4_place: votes[VOTE_INDEX.PLACE_4] || 0, + id_5_place: votes[VOTE_INDEX.PLACE_5] || 0, + id_6_place: votes[VOTE_INDEX.PLACE_6] || 0, + id_7_place: votes[VOTE_INDEX.PLACE_7] || 0, + id_8_place: votes[VOTE_INDEX.PLACE_8] || 0, + id_fast_lap: votes[VOTE_INDEX.FAST_LAP] || 0, + id_dnf: votes[VOTE_INDEX.DNF] || 0, + constructor_id: votes[VOTE_INDEX.CONSTRUCTOR] || 0 + }; + } + + /** + * Safely converts any value to a number, returning 0 for invalid inputs. + */ + static toNumber(value: any): number { + return isNaN(+value) ? 0 : +value; + } +} \ No newline at end of file diff --git a/client/src/app/model/leaderboard.ts b/client/src/app/model/leaderboard.ts new file mode 100644 index 000000000..b7dda4e29 --- /dev/null +++ b/client/src/app/model/leaderboard.ts @@ -0,0 +1,7 @@ +export interface LeaderBoard{ + id: number, + username: string; + points: number; + numberVotes: number; + avatarImage?: string; + } \ No newline at end of file diff --git a/client/src/app/model/track.ts b/client/src/app/model/track.ts new file mode 100644 index 000000000..89cb9e549 --- /dev/null +++ b/client/src/app/model/track.ts @@ -0,0 +1,33 @@ +// Track interface +export interface TrackData { + track_id: number; + name: string; + date: string; + has_sprint: number; + has_x2: number; + country: string; + besttime_driver_time: string; + username: string; +} + +// Cumulative points interface +export interface CumulativePointsData { + date: string; + track_name: string; + driver_id: number; + driver_username: string; + driver_color: string; + cumulative_points: number; +} + +export interface DriverOfWeek { + driver_username: string; + driver_id: number; + points: number; +} + +export interface ConstructorOfWeek { + constructor_name: string; + constructor_id: number; + points: number; +} diff --git a/client/src/app/service/api.service.spec.ts b/client/src/app/service/api.service.spec.ts new file mode 100644 index 000000000..e36deb42b --- /dev/null +++ b/client/src/app/service/api.service.spec.ts @@ -0,0 +1,44 @@ +import { TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { ApiService } from './api.service'; + +describe('ApiService', () => { + let service: ApiService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [provideNoopAnimations(), provideHttpClient(), + provideHttpClientTesting() + ] + }); + service = TestBed.inject(ApiService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should return correct base URL', () => { + expect(service.getBaseUrl()).toBeDefined(); + }); + + it('should construct endpoint URLs correctly', () => { + const endpoint = 'AuthService/login'; + const url = service.getEndpointUrl(endpoint); + expect(url).toContain(endpoint); + }); + + it('should create headers with content type', () => { + const headers = service.createHeaders(); + expect(headers['Content-Type']).toBe('application/json'); + }); + + it('should create auth headers with bearer token', () => { + const token = 'test-token'; + const headers = service.createAuthHeaders(token); + expect(headers['Authorization']).toBe(`Bearer ${token}`); + }); +}); diff --git a/client/src/app/service/api.service.ts b/client/src/app/service/api.service.ts new file mode 100644 index 000000000..d1c5db35d --- /dev/null +++ b/client/src/app/service/api.service.ts @@ -0,0 +1,92 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { ConfigService } from './config.service'; + +@Injectable({ + providedIn: 'root' +}) +export class ApiService { + private http = inject(HttpClient); + private configService = inject(ConfigService); + + private readonly baseUrl: string; + + constructor() { + this.baseUrl = this.configService.apiBaseUrl; + } + + /** + * Get the base URL for API calls + */ + getBaseUrl(): string { + return this.baseUrl; + } + + /** + * Get the full URL for a specific endpoint + */ + getEndpointUrl(endpoint: string): string { + return `${this.baseUrl}/${endpoint.startsWith('/') ? endpoint.slice(1) : endpoint}`; + } + + /** + * Make a POST request to an endpoint + */ + post(endpoint: string, body: unknown, headers?: Record): Observable { + headers = { ...headers, ...this.setHeaderToken() }; + const options = headers ? { headers: new HttpHeaders(headers) } : {}; + return this.http.post(this.getEndpointUrl(endpoint), body, options); + } + + /** + * Make a GET request to an endpoint + */ + get(endpoint: string, headers?: Record): Observable { + headers = { ...headers, ...this.setHeaderToken() }; + const options = headers ? { headers: new HttpHeaders(headers) } : {}; + return this.http.get(this.getEndpointUrl(endpoint), options); + } + + /** + * Make a PUT request to an endpoint + */ + put(endpoint: string, body: unknown, headers?: Record): Observable { + headers = { ...headers, ...this.setHeaderToken() }; + const options = headers ? { headers: new HttpHeaders(headers) } : {}; + return this.http.put(this.getEndpointUrl(endpoint), body, options); + } + + /** + * Make a DELETE request to an endpoint + */ + delete(endpoint: string, headers?: Record): Observable { + headers = { ...headers, ...this.setHeaderToken() }; + const options = headers ? { headers: new HttpHeaders(headers) } : {}; + return this.http.delete(this.getEndpointUrl(endpoint), options); + } + + private setHeaderToken(): Record { + const match = document.cookie.match(/(^|;) ?authToken=([^;]*)(;|$)/); + return match ? this.createAuthHeaders(match[2]) : this.createHeaders(); + } + + /** + * Create headers with authentication token + */ + createAuthHeaders(token: string): Record { + return { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }; + } + + /** + * Create headers for API requests + */ + createHeaders(): Record { + return { + 'Content-Type': 'application/json' + }; + } +} diff --git a/client/src/app/service/auth.constants.ts b/client/src/app/service/auth.constants.ts new file mode 100644 index 000000000..589278248 --- /dev/null +++ b/client/src/app/service/auth.constants.ts @@ -0,0 +1,10 @@ +export const AUTH_CONSTANTS = { + COOKIE_NAME: 'authToken', + COOKIE_PATH: '/', + COOKIE_MAX_AGE: 7 * 24 * 60 * 60, // 7 days in seconds + + STORAGE_KEYS: { + USER: 'user', + IS_LOGGED_IN: 'isLoggedIn' + } +} as const; diff --git a/client/src/app/service/auth.service.spec.ts b/client/src/app/service/auth.service.spec.ts new file mode 100644 index 000000000..b802d363b --- /dev/null +++ b/client/src/app/service/auth.service.spec.ts @@ -0,0 +1,19 @@ +import { TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; + +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [provideNoopAnimations(), ],}); + service = TestBed.inject(AuthService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/client/src/app/service/auth.service.ts b/client/src/app/service/auth.service.ts new file mode 100644 index 000000000..e42775dc8 --- /dev/null +++ b/client/src/app/service/auth.service.ts @@ -0,0 +1,395 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { Router } from '@angular/router'; +import { firstValueFrom } from 'rxjs'; +import type { + AuthResponse, + ChangePasswordRequest, + ChangePasswordResponse, + LoginRequest, + LogoutResponse, + RefreshTokenResponse, + RegisterRequest, + SessionsResponse, + TokenValidationResponse, + UpdateUserInfoRequest, + User +} from '@f123dashboard/shared'; +import { ApiService } from './api.service'; +import { ConfigService } from './config.service'; +import { CookieService } from './cookie.service'; +import { AUTH_CONSTANTS } from './auth.constants'; +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + private router = inject(Router); + private apiService = inject(ApiService); + private configService = inject(ConfigService); + private cookieService = inject(CookieService); + + private currentUserSignal = signal(null); + private isAuthenticatedSignal = signal(false); + private tokenRefreshTimer: ReturnType | undefined; + + public currentUser = this.currentUserSignal.asReadonly(); + public isAuthenticated = this.isAuthenticatedSignal.asReadonly(); + + constructor() { + this.initializeAuth(); + } + + private getClientInfo(): { userAgent: string } { + return { + userAgent: navigator.userAgent + }; + } + + private initializeAuth(): void { + const token = this.getToken(); + const storedUser = sessionStorage.getItem(AUTH_CONSTANTS.STORAGE_KEYS.USER); + + if (storedUser === null) { + return; + } + try { + const user = JSON.parse(storedUser) as User; + this.currentUserSignal.set(user); + const hasEmail = !!(user.mail && user.mail.trim() !== ''); + this.isAuthenticatedSignal.set(hasEmail); + } catch (error) { + console.error('Error parsing stored user data:', error); + } + + + if (token) { + this.validateTokenAndSetUser(); + } + } + + private async validateTokenAndSetUser(): Promise { + try { + const validation = await firstValueFrom( + this.apiService.post('/auth/validate', {}) + ); + if (validation.valid && validation.userId && validation.username && validation.name && validation.surname) { + const userData: User = { + id: validation.userId, + username: validation.username, + name: validation.name, + surname: validation.surname, + mail: validation.mail, + image: validation.image, + isAdmin: validation.isAdmin + }; + + this.setAuthenticatedUser(userData); + this.scheduleTokenRefresh(); + } else { + this.clearAuth(); + } + } catch (error) { + console.error('Token validation error:', error); + this.clearAuth(); + } + } + + async login(loginData: LoginRequest, skipNavigation = false): Promise { + try { + const clientInfo = this.getClientInfo(); + loginData.userAgent = clientInfo.userAgent; + const response = await firstValueFrom( + this.apiService.post('/auth/login', loginData) + ); + + if (response.success && response.user && response.token) { + this.setToken(response.token); + this.scheduleTokenRefresh(); + if (response.user.mail && response.user.mail.trim() !== '') { + this.setAuthenticatedUser(response.user); + } + + if (!skipNavigation) { + const returnUrl = response.user.isAdmin ? '/admin' : '/fanta'; + this.router.navigate([returnUrl]); + } + } + return response; + } catch (error) { + console.error('Login error:', error); + return { + success: false, + message: 'Si è verificato un errore durante il login. Riprova.' + }; + } + } + + async register(registerData: RegisterRequest): Promise { + try { + const response = await firstValueFrom( + this.apiService.post('/auth/register', registerData) + ); + + if (response.success && response.user && response.token) { + this.setToken(response.token); + this.setAuthenticatedUser(response.user); + this.scheduleTokenRefresh(); + + // Navigate to dashboard + this.router.navigate(['/dashboard']); + } + + return response; + } catch (error: unknown) { + console.error('Registration error:', error); + + // Handle HTTP error responses + if (error && typeof error === 'object' && 'error' in error) { + const httpError = error as { error?: { message?: string } }; + if (httpError.error?.message) { + return { + success: false, + message: httpError.error.message + }; + } + } + + return { + success: false, + message: 'Si è verificato un errore durante la registrazione. Riprova. Se l\'errore persiste, contatta il supporto.' + }; + } + } + + async changePassword(currentPassword: string, newPassword: string): Promise { + try { + const token = this.getToken(); + if (!token) { + return { + success: false, + message: 'Token di autenticazione non trovato' + }; + } + + const changeData: ChangePasswordRequest = { + currentPassword, + newPassword, + jwtToken: token + }; + + const response = await firstValueFrom( + this.apiService.post('/auth/change-password', changeData) + ); + return response; + } catch (error) { + console.error('Change password error:', error); + return { + success: false, + message: 'Si è verificato un errore durante il cambio password. Riprova.' + }; + } + } + + async updateUserInfo(updateData: UpdateUserInfoRequest): Promise { + try { + + const response = await firstValueFrom( + this.apiService.post('/auth/update-user-info', updateData) + ); + + // Update the current user data in the service + if (response.success && response.user) { + this.setAuthenticatedUser(response.user); + } + + return response; + } catch (error: unknown) { + console.error('Update user info error:', error); + + // Handle HTTP error responses + if (error && typeof error === 'object' && 'error' in error) { + const httpError = error as { error?: { message?: string } }; + if (httpError.error?.message) { + return { + success: false, + message: httpError.error.message + }; + } + } + + return { + success: false, + message: 'Si è verificato un errore durante l\'aggiornamento delle informazioni utente. Riprova.' + }; + } + } + + async refreshToken(): Promise { + try { + const currentToken = this.getToken(); + if (!currentToken) { + return false; + } + + const clientInfo = this.getClientInfo(); + const response = await firstValueFrom( + this.apiService.post('/auth/refresh-token', { + token: currentToken, + userAgent: clientInfo.userAgent + }) + ); + + if (response.success && response.token) { + this.setToken(response.token); + this.scheduleTokenRefresh(); + return true; + } + + return false; + } catch (error) { + console.error('Token refresh error:', error); + return false; + } + } + + async logout(): Promise { + try { + const token = this.getToken(); + // Call backend to invalidate session + if (token) { + await firstValueFrom( + this.apiService.post('/auth/logout', {}) + ); + } + } catch (error) { + console.warn('Backend logout error:', error); + } finally { + // Always clear local auth state + this.clearAuth(); + // Use setTimeout to avoid ExpressionChangedAfterItHasBeenCheckedError + setTimeout(() => { + this.router.navigate(['/login']); + }, 0); + } + } + + private setAuthenticatedUser(user: User): void { + this.currentUserSignal.set(user); + + // Only set as authenticated if user has email + const hasEmail = !!(user.mail && user.mail.trim() !== ''); + this.isAuthenticatedSignal.set(hasEmail); + + // Store user data in session storage for persistence + sessionStorage.setItem(AUTH_CONSTANTS.STORAGE_KEYS.USER, JSON.stringify(user)); + sessionStorage.setItem(AUTH_CONSTANTS.STORAGE_KEYS.IS_LOGGED_IN, hasEmail ? 'true' : 'false'); + } + + // Method to mark user as fully authenticated after email completion + public markUserAsAuthenticated(): void { + const currentUser = this.currentUserSignal(); + if (currentUser && currentUser.mail && currentUser.mail.trim() !== '') { + this.isAuthenticatedSignal.set(true); + sessionStorage.setItem(AUTH_CONSTANTS.STORAGE_KEYS.IS_LOGGED_IN, 'true'); + } + } + + private clearAuth(): void { + this.currentUserSignal.set(null); + this.isAuthenticatedSignal.set(false); + + // Clear stored data + this.cookieService.deleteCookie(AUTH_CONSTANTS.COOKIE_NAME, AUTH_CONSTANTS.COOKIE_PATH); + sessionStorage.removeItem(AUTH_CONSTANTS.STORAGE_KEYS.USER); + sessionStorage.removeItem(AUTH_CONSTANTS.STORAGE_KEYS.IS_LOGGED_IN); + + // Clear token refresh timer + if (this.tokenRefreshTimer) { + clearTimeout(this.tokenRefreshTimer); + } + } + + private setToken(token: string): void { + this.cookieService.setCookie(AUTH_CONSTANTS.COOKIE_NAME, token, { + maxAge: AUTH_CONSTANTS.COOKIE_MAX_AGE, + path: AUTH_CONSTANTS.COOKIE_PATH, + secure: true, + sameSite: 'Strict' + }); + } + + public getToken(): string | null { + return this.cookieService.getCookie(AUTH_CONSTANTS.COOKIE_NAME); + } + + private scheduleTokenRefresh(): void { + // Clear existing timer + if (this.tokenRefreshTimer) { + clearTimeout(this.tokenRefreshTimer); + } + + // Schedule refresh using configured time before expiry + this.tokenRefreshTimer = setTimeout(() => { + this.refreshToken().then(success => { + if (!success) { + this.logout(); + } + }); + }, this.configService.tokenRefreshTimeBeforeExpiry); + } + + // Session management methods + async getUserSessions(): Promise { + try { + const token = this.getToken(); + if (!token) { + return { success: false, message: 'Token di autenticazione non trovato' }; + } + + const response = await firstValueFrom( + this.apiService.get('/auth/sessions') + ); + return response; + } catch (error) { + console.error('Get user sessions error:', error); + return { success: false, message: 'Si è verificato un errore durante il recupero delle sessioni' }; + } + } + + async logoutAllSessions(): Promise { + try { + const token = this.getToken(); + if (!token) { + return { success: false, message: 'Token di autenticazione non trovato' }; + } + + const response = await firstValueFrom( + this.apiService.post('/auth/logout-all-sessions', {}) + ); + + if (response.success) { + // Clear local auth state since all sessions are logged out + this.clearAuth(); + // Use setTimeout to avoid ExpressionChangedAfterItHasBeenCheckedError + setTimeout(() => { + this.router.navigate(['/login']); + }, 0); + } + + return response; + } catch (error) { + console.error('Logout all sessions error:', error); + return { success: false, message: 'Si è verificato un errore durante il logout da tutte le sessioni' }; + } + } + + + // Helper method to validate password strength + isPasswordStrong(password: string): boolean { + const hasUpperCase = /[A-Z]/.test(password); + const hasLowerCase = /[a-z]/.test(password); + const hasMinLength = password.length >= 8; + const hasNumbers = /\d/.test(password); + + return hasUpperCase && hasLowerCase && hasNumbers && hasMinLength; + } +} diff --git a/client/src/app/service/config.service.spec.ts b/client/src/app/service/config.service.spec.ts new file mode 100644 index 000000000..d2b68a173 --- /dev/null +++ b/client/src/app/service/config.service.spec.ts @@ -0,0 +1,32 @@ +import { TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { ConfigService } from './config.service'; + +describe('ConfigService', () => { + let service: ConfigService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [provideNoopAnimations(), ],}); + service = TestBed.inject(ConfigService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should provide API base URL', () => { + expect(service.apiBaseUrl).toBeDefined(); + expect(typeof service.apiBaseUrl).toBe('string'); + }); + + it('should provide production flag', () => { + expect(typeof service.isProduction).toBe('boolean'); + }); + + it('should provide token refresh configuration', () => { + expect(typeof service.tokenRefreshTimeBeforeExpiry).toBe('number'); + expect(service.tokenRefreshTimeBeforeExpiry).toBeGreaterThan(0); + }); +}); diff --git a/client/src/app/service/config.service.ts b/client/src/app/service/config.service.ts new file mode 100644 index 000000000..4ee6d5f93 --- /dev/null +++ b/client/src/app/service/config.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { environment } from '../../environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class ConfigService { + + get apiBaseUrl(): string { + return environment.apiBaseUrl; + } + + get isProduction(): boolean { + return environment.production; + } + + get tokenRefreshTimeBeforeExpiry(): number { + return 6 * 24 * 60 * 60 * 1000; // 6 days in milliseconds + } +} diff --git a/client/src/app/service/constructor.service.spec.ts b/client/src/app/service/constructor.service.spec.ts new file mode 100644 index 000000000..0e823bca3 --- /dev/null +++ b/client/src/app/service/constructor.service.spec.ts @@ -0,0 +1,207 @@ +import { TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { ConstructorService } from './constructor.service'; +import type { Constructor, DriverData } from '@f123dashboard/shared'; + +describe('ConstructorService', () => { + let service: ConstructorService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [provideNoopAnimations(), ],}); + service = TestBed.inject(ConstructorService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('calculateConstructorPoints', () => { + it('should calculate constructor points correctly from two drivers', () => { + const drivers: DriverData[] = [ + { + driver_id: 1, + driver_username: 'driver1', + total_race_points: 100, + total_full_race_points: 50, + total_sprint_points: 30, + total_qualifying_points: 20, + total_free_practice_points: 10, + total_points: 210 + } as DriverData, + { + driver_id: 2, + driver_username: 'driver2', + total_race_points: 80, + total_full_race_points: 40, + total_sprint_points: 25, + total_qualifying_points: 15, + total_free_practice_points: 8, + total_points: 168 + } as DriverData + ]; + + const constructors: Constructor[] = [ + { + constructor_id: 1, + constructor_name: 'Team A', + driver_1_username: 'driver1', + driver_2_username: 'driver2', + driver_1_id: 1, + driver_2_id: 2, + constructor_race_points: 0, + constructor_full_race_points: 0, + constructor_sprint_points: 0, + constructor_qualifying_points: 0, + constructor_free_practice_points: 0, + constructor_tot_points: 0 + } as Constructor + ]; + + const result = service.calculateConstructorPoints(constructors, drivers); + + expect(result[0].constructor_race_points).toBe(180); // 100 + 80 + expect(result[0].constructor_full_race_points).toBe(90); // 50 + 40 + expect(result[0].constructor_sprint_points).toBe(55); // 30 + 25 + expect(result[0].constructor_qualifying_points).toBe(35); // 20 + 15 + expect(result[0].constructor_free_practice_points).toBe(18); // 10 + 8 + expect(result[0].constructor_tot_points).toBe(378); // 210 + 168 + }); + + it('should handle null/undefined points gracefully', () => { + const drivers: DriverData[] = [ + { + driver_id: 1, + driver_username: 'driver1', + total_points: 100 + } as DriverData, + { + driver_id: 2, + driver_username: 'driver2', + total_points: 80 + } as DriverData + ]; + + const constructors: Constructor[] = [ + { + constructor_id: 1, + constructor_name: 'Team A', + driver_1_username: 'driver1', + driver_2_username: 'driver2', + driver_1_id: 1, + driver_2_id: 2 + } as Constructor + ]; + + const result = service.calculateConstructorPoints(constructors, drivers); + + expect(result[0].constructor_race_points).toBe(0); + expect(result[0].constructor_tot_points).toBe(180); // 100 + 80 + }); + + it('should sort constructors by total points descending', () => { + const drivers: DriverData[] = [ + { driver_id: 1, driver_username: 'driver1', total_points: 50 } as DriverData, + { driver_id: 2, driver_username: 'driver2', total_points: 60 } as DriverData, + { driver_id: 3, driver_username: 'driver3', total_points: 100 } as DriverData, + { driver_id: 4, driver_username: 'driver4', total_points: 90 } as DriverData + ]; + + const constructors: Constructor[] = [ + { + constructor_id: 1, + constructor_name: 'Team A', + driver_1_username: 'driver1', + driver_2_username: 'driver2' + } as Constructor, + { + constructor_id: 2, + constructor_name: 'Team B', + driver_1_username: 'driver3', + driver_2_username: 'driver4' + } as Constructor + ]; + + const result = service.calculateConstructorPoints(constructors, drivers); + + expect(result[0].constructor_name).toBe('Team B'); // 190 points + expect(result[0].constructor_tot_points).toBe(190); + expect(result[1].constructor_name).toBe('Team A'); // 110 points + expect(result[1].constructor_tot_points).toBe(110); + }); + + it('should not calculate points if one driver is missing', () => { + const drivers: DriverData[] = [ + { driver_id: 1, driver_username: 'driver1', total_points: 100 } as DriverData + ]; + + const constructors: Constructor[] = [ + { + constructor_id: 1, + constructor_name: 'Team A', + driver_1_username: 'driver1', + driver_2_username: 'missingDriver' + } as Constructor + ]; + + const result = service.calculateConstructorPoints(constructors, drivers); + + expect(result[0].constructor_tot_points).toBeUndefined(); + }); + }); + + describe('calculateConstructorGainedPoints', () => { + it('should calculate gained points correctly', () => { + const drivers = [ + { driver_username: 'driver1', gainedPoints: 25 }, + { driver_username: 'driver2', gainedPoints: 30 }, + { driver_username: 'driver3', gainedPoints: 15 } + ]; + + const constructors: Constructor[] = [ + { + constructor_id: 1, + constructor_name: 'Team A', + driver_1_username: 'driver1', + driver_2_username: 'driver2' + } as Constructor, + { + constructor_id: 2, + constructor_name: 'Team B', + driver_1_username: 'driver3', + driver_2_username: 'driver1' + } as Constructor + ]; + + const result = service.calculateConstructorGainedPoints(constructors, drivers); + + expect(result[0].constructor_race_points).toBe(55); // 25 + 30 + expect(result[1].constructor_race_points).toBe(40); // 15 + 25 + }); + + it('should initialize to 0 if no matching drivers', () => { + const drivers = [ + { driver_username: 'driver1', gainedPoints: 25 } + ]; + + const constructors: Constructor[] = [ + { + constructor_id: 1, + constructor_name: 'Team A', + driver_1_username: 'otherDriver1', + driver_2_username: 'otherDriver2' + } as Constructor + ]; + + const result = service.calculateConstructorGainedPoints(constructors, drivers); + + expect(result[0].constructor_race_points).toBe(0); + }); + + it('should handle empty arrays', () => { + const result = service.calculateConstructorGainedPoints([], []); + expect(result).toEqual([]); + }); + }); +}); diff --git a/client/src/app/service/constructor.service.ts b/client/src/app/service/constructor.service.ts new file mode 100644 index 000000000..0a427d4f1 --- /dev/null +++ b/client/src/app/service/constructor.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@angular/core'; +import type { Constructor, DriverData } from '@f123dashboard/shared'; + +@Injectable({ + providedIn: 'root' +}) +export class ConstructorService { + /** + * Calculates all point types for each constructor based on their drivers' data + * @param constructors Array of constructors to calculate points for + * @param drivers Array of driver data containing points information + * @returns Sorted array of constructors with calculated points + */ + calculateConstructorPoints(constructors: Constructor[], drivers: DriverData[]): Constructor[] { + const updatedConstructors = constructors.map(constructor => { + // Find the drivers for this constructor + const driver1 = drivers.find(d => d.driver_username === constructor.driver_1_username); + const driver2 = drivers.find(d => d.driver_username === constructor.driver_2_username); + + if (driver1 && driver2) { + // Sum session points + constructor.constructor_race_points = Number(driver1.total_race_points ?? 0) + Number(driver2.total_race_points ?? 0); + constructor.constructor_full_race_points = Number(driver1.total_full_race_points ?? 0) + Number(driver2.total_full_race_points ?? 0); + constructor.constructor_sprint_points = Number(driver1.total_sprint_points ?? 0) + Number(driver2.total_sprint_points ?? 0); + constructor.constructor_qualifying_points = Number(driver1.total_qualifying_points ?? 0) + Number(driver2.total_qualifying_points ?? 0); + constructor.constructor_free_practice_points = Number(driver1.total_free_practice_points ?? 0) + Number(driver2.total_free_practice_points ?? 0); + + constructor.constructor_tot_points = Number(driver1.total_points ?? 0) + Number(driver2.total_points ?? 0); + } + + return constructor; + }); + + // Sort constructors by total points + return updatedConstructors.sort((a, b) => b.constructor_tot_points - a.constructor_tot_points); + } + + /** + * Calculates gained points for constructors based on driver gained points + * Used for "Driver of the Week" calculations + * @param constructors Array of constructors + * @param drivers Array of drivers with gainedPoints property + * @returns Constructors with updated constructor_race_points (gained points) + */ + calculateConstructorGainedPoints( + constructors: Constructor[], + drivers: { driver_username: string; gainedPoints: number }[] + ): Constructor[] { + return constructors.map(constructor => { + constructor.constructor_race_points = 0; + for (const driver of drivers) { + if (driver.driver_username === constructor.driver_1_username || driver.driver_username === constructor.driver_2_username) { + constructor.constructor_race_points += driver.gainedPoints; + } + } + return constructor; + }); + } +} diff --git a/client/src/app/service/cookie.service.spec.ts b/client/src/app/service/cookie.service.spec.ts new file mode 100644 index 000000000..6adfe9a32 --- /dev/null +++ b/client/src/app/service/cookie.service.spec.ts @@ -0,0 +1,63 @@ +import { TestBed } from '@angular/core/testing'; +import { CookieService } from './cookie.service'; + +describe('CookieService', () => { + let service: CookieService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CookieService); + + // Clear all cookies before each test + document.cookie.split(';').forEach(cookie => { + const name = cookie.split('=')[0].trim(); + document.cookie = `${name}=; max-age=0; path=/`; + }); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should set a cookie', () => { + service.setCookie('testCookie', 'testValue', { secure: false }); + expect(document.cookie).toContain('testCookie=testValue'); + }); + + it('should get a cookie', () => { + document.cookie = 'testCookie=testValue; path=/'; + const value = service.getCookie('testCookie'); + expect(value).toBe('testValue'); + }); + + it('should return null for non-existent cookie', () => { + const value = service.getCookie('nonExistent'); + expect(value).toBeNull(); + }); + + it('should delete a cookie', () => { + service.setCookie('testCookie', 'testValue', { secure: false }); + expect(service.hasCookie('testCookie')).toBe(true); + + service.deleteCookie('testCookie'); + expect(service.hasCookie('testCookie')).toBe(false); + }); + + it('should check if cookie exists', () => { + expect(service.hasCookie('testCookie')).toBe(false); + + service.setCookie('testCookie', 'testValue', { secure: false }); + expect(service.hasCookie('testCookie')).toBe(true); + }); + + it('should set cookie with custom options', () => { + service.setCookie('testCookie', 'testValue', { + maxAge: 3600, + path: '/', + secure: false, + sameSite: 'Lax' + }); + + expect(document.cookie).toContain('testCookie=testValue'); + }); +}); diff --git a/client/src/app/service/cookie.service.ts b/client/src/app/service/cookie.service.ts new file mode 100644 index 000000000..d7e4eb729 --- /dev/null +++ b/client/src/app/service/cookie.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class CookieService { + setCookie(name: string, value: string, options: { + maxAge?: number; + path?: string; + secure?: boolean; + sameSite?: 'Strict' | 'Lax' | 'None'; + } = {}): void { + const { + maxAge, + path = '/', + secure = true, + sameSite = 'Strict' + } = options; + + let cookieString = `${name}=${value}; path=${path}`; + + if (maxAge !== undefined) { + cookieString += `; max-age=${maxAge}`; + } + + if (secure) { + cookieString += '; Secure'; + } + + if (sameSite) { + cookieString += `; SameSite=${sameSite}`; + } + + document.cookie = cookieString; + } + + getCookie(name: string): string | null { + const match = document.cookie.match(new RegExp(`(^|;) ?${name}=([^;]*)(;|$)`)); + return match ? match[2] : null; + } + + deleteCookie(name: string, path = '/'): void { + document.cookie = `${name}=; path=${path}; max-age=0`; + } + + hasCookie(name: string): boolean { + return this.getCookie(name) !== null; + } +} diff --git a/client/src/app/service/db-data.service.spec.ts b/client/src/app/service/db-data.service.spec.ts new file mode 100644 index 000000000..7d1e62ed3 --- /dev/null +++ b/client/src/app/service/db-data.service.spec.ts @@ -0,0 +1,19 @@ +import { TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; + +import { DbDataService } from './db-data.service'; + +describe('DbDataService', () => { + let service: DbDataService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [provideNoopAnimations(), ],}); + service = TestBed.inject(DbDataService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/client/src/app/service/db-data.service.ts b/client/src/app/service/db-data.service.ts new file mode 100644 index 000000000..0fa859d39 --- /dev/null +++ b/client/src/app/service/db-data.service.ts @@ -0,0 +1,247 @@ +import { Injectable, inject, signal, computed } from '@angular/core'; +import { firstValueFrom, forkJoin } from 'rxjs'; +import type { DriverData, Driver, ChampionshipData, Season, CumulativePointsData, TrackData, User, RaceResult, ConstructorGrandPrixPoints, Constructor } from '@f123dashboard/shared'; +import { GpResult } from '../model/championship'; +import { ApiService } from './api.service'; +import { AuthService } from './auth.service'; + +@Injectable({ + providedIn: 'root' +}) +export class DbDataService { + private apiService = inject(ApiService); + +/****************************************************************/ +//variabili locali +/****************************************************************/ + private allDriversSignal = signal([]); + readonly allDrivers = this.allDriversSignal.asReadonly(); + + private championshipSignal = signal([]); + readonly championship = this.championshipSignal.asReadonly(); + + private cumulativePointsSignal = signal([]); + readonly cumulativePoints = this.cumulativePointsSignal.asReadonly(); + + private tracksSignal = signal([]); + readonly tracks = this.tracksSignal.asReadonly(); + + private raceResultSignal = signal([]); + readonly raceResult = this.raceResultSignal.asReadonly(); + + private usersSignal = signal([]); + readonly users = this.usersSignal.asReadonly(); + + private driversSignal = signal([]); + readonly drivers = this.driversSignal.asReadonly(); + + private constructorGrandPrixPointsSignal = signal([]); + readonly constructorGrandPrixPoints = this.constructorGrandPrixPointsSignal.asReadonly(); + + private constructorsSignal = signal([]); + readonly constructors = computed(() => { + const constructors = this.constructorsSignal(); + // TODO fix hardcoded drivers in constructors + return constructors.map(constructor => { + const updated = { ...constructor }; + if (constructor.constructor_id === 1) { + updated.driver_1_id = 10; + updated.driver_1_username = "Marcogang96"; + updated.driver_2_id = 11; + updated.driver_2_username = "GiannisCorbe"; + } else if (constructor.constructor_id === 4) { + updated.driver_1_id = 14; + updated.driver_1_username = "redmamba_99_"; + updated.driver_2_id = 16; + updated.driver_2_username = "JJKudos"; + } else if (constructor.constructor_id === 2) { + updated.driver_1_id = 12; + updated.driver_1_username = "Lil Mvrck"; + updated.driver_2_id = 17; + updated.driver_2_username = "Octimus10"; + } else if (constructor.constructor_id === 3) { + updated.driver_1_id = 13; + updated.driver_1_username = "FASTman"; + updated.driver_2_id = 15; + updated.driver_2_username = "Dreandos"; + } + return updated; + }); + }); + + readonly winningConstructorGrandPrixPoints = computed(() => { + const allConstructorGpPoints = this.constructorGrandPrixPoints(); + const winningConstructorsByRace = new Map(); + + allConstructorGpPoints.forEach(entry => { + const existingWinner = winningConstructorsByRace.get(entry.grand_prix_id); + if (!existingWinner || entry.constructor_points > existingWinner.constructor_points) { + winningConstructorsByRace.set(entry.grand_prix_id, entry); + } + }); + + return Array.from(winningConstructorsByRace.values()) + .sort((a, b) => new Date(a.grand_prix_date).getTime() - new Date(b.grand_prix_date).getTime()); + }); + + +/****************************************************************/ +//compilazione delle variabili pre caricamento del interfaccia web +/****************************************************************/ + + async allData() { + const { + allDrivers, + championship, + cumulativePoints, + tracks, + users, + raceResult, + constructors, + drivers, + constructorGrandPrixPoints + } = await firstValueFrom(forkJoin({ + allDrivers: this.apiService.post('/database/drivers', {}), + championship: this.apiService.post('/database/championship', {}), + cumulativePoints: this.apiService.post('/database/cumulative-points', {}), + tracks: this.apiService.post('/database/tracks', {}), + users: this.apiService.post('/auth/users', {}), + raceResult: this.apiService.post('/database/race-results', {}), + constructors: this.apiService.post('/database/constructors', {}), + drivers: this.apiService.post('/database/drivers-data', {}), + constructorGrandPrixPoints: this.apiService.post('/database/constructor-grand-prix-points', {}) + })); + + this.allDriversSignal.set(allDrivers); + this.championshipSignal.set(championship); + this.cumulativePointsSignal.set(cumulativePoints); + this.tracksSignal.set(tracks); + this.usersSignal.set(users); + this.raceResultSignal.set(raceResult); + this.constructorsSignal.set(constructors); + this.driversSignal.set(drivers); + this.constructorGrandPrixPointsSignal.set(constructorGrandPrixPoints); + } + +/****************************************************************/ +//season-specific data methods +/****************************************************************/ + + async getDriversBySeason(seasonId?: number): Promise { + const drivers = await firstValueFrom( + this.apiService.post('/database/drivers', { seasonId }) + ); + return drivers; + } + + async getDriversData(seasonId?: number): Promise { + const drivers = await firstValueFrom( + this.apiService.post('/database/drivers-data', { seasonId }) + ); + return drivers; + } + + async getChampionshipBySeason(seasonId?: number): Promise { + const championship = await firstValueFrom( + this.apiService.post('/database/championship', { seasonId }) + ); + return championship; + } + + async getCumulativePointsBySeason(seasonId?: number): Promise { + const cumulativePoints = await firstValueFrom( + this.apiService.post('/database/cumulative-points', { seasonId }) + ); + return cumulativePoints; + } + + async getAllTracksBySeason(seasonId?: number): Promise { + const tracks = await firstValueFrom( + this.apiService.post('/database/tracks', { seasonId }) + ); + return tracks; + } + + async getRaceResultBySeason(seasonId?: number): Promise { + const raceResult = await firstValueFrom( + this.apiService.post('/database/race-results', { seasonId }) + ); + return raceResult; + } + + async getAllSeasons(): Promise { + const seasons = await firstValueFrom( + this.apiService.post('/database/seasons', {}) + ); + return seasons; + } + + async getConstructorsBySeason(seasonId?: number): Promise { + const constructors = await firstValueFrom( + this.apiService.post('/database/constructors', { seasonId }) + ); + return constructors; + } + + async getConstructorGrandPrixPoints(seasonId?: number): Promise { + const constructorGpPoints = await firstValueFrom( + this.apiService.post('/database/constructor-grand-prix-points', { seasonId }) + ); + return constructorGpPoints; + } + + async setGpResult(trackId: number, gpResult: GpResult): Promise { + try { + const result = await firstValueFrom( + this.apiService.post('/database/set-gp-result', { + trackId: +trackId, + hasSprint: gpResult.hasSprint, + raceResult: gpResult.raceResult, + raceDnfResult: gpResult.raceDnfResult, + sprintResult: gpResult.sprintResult, + sprintDnfResult: gpResult.sprintDnfResult, + qualiResult: gpResult.qualiResult, + fpResult: gpResult.fpResult, + seasonId: gpResult.seasonId + }) + ); + return result; + } catch (error) { + console.error('Error setting GP result:', error); + throw new Error(`Failed to set GP result: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + + getAvatarSrc(user: User | null): string { + if (user?.image) { + // Handle case where image might be a Buffer or other non-string type + const image = user.image as any; // Type assertion since image can be Buffer from DB + let imageStr: string; + + if (typeof image === 'string') { + imageStr = image; + } else if (image instanceof ArrayBuffer || image instanceof Uint8Array) { + // Convert Buffer/ArrayBuffer to base64 string + const bytes = new Uint8Array(image); + imageStr = btoa(String.fromCharCode(...bytes)); + } else if (image && typeof image === 'object' && 'data' in image) { + // Handle Buffer-like object with data property + const bytes = new Uint8Array(image.data); + imageStr = btoa(String.fromCharCode(...bytes)); + } else { + // Try to convert to string + imageStr = String(image); + } + + // Check if it's already a data URL (base64) + if (imageStr.startsWith('data:')) { + return imageStr; + } + // If it's base64 without data URL prefix, add it + return `data:image/jpeg;base64,${imageStr}`; + } + // Fallback to file path + return `./assets/images/avatars_fanta/${user?.id || 'default'}.png`; + } + +} diff --git a/client/src/app/service/fanta.service.spec.ts b/client/src/app/service/fanta.service.spec.ts new file mode 100644 index 000000000..e8e34db7a --- /dev/null +++ b/client/src/app/service/fanta.service.spec.ts @@ -0,0 +1,683 @@ +import { TestBed } from '@angular/core/testing'; +import { signal, WritableSignal, computed } from '@angular/core'; +import { of } from 'rxjs'; +import { FantaService } from './fanta.service'; +import { DbDataService } from './db-data.service'; +import { ApiService } from './api.service'; +import type { FantaVote, RaceResult, Driver, ConstructorGrandPrixPoints } from '@f123dashboard/shared'; + +describe('FantaService', () => { + let service: FantaService; + let mockDbDataService: Partial; + let mockApiService: jasmine.SpyObj; + let driversSignal: WritableSignal; + let constructorGrandPrixPointsSignal: WritableSignal; + let raceResultSignal: WritableSignal; + + const mockDrivers: Driver[] = [ + { id: 1, username: 'driver1', first_name: 'John', surname: 'Doe' }, + { id: 2, username: 'driver2', first_name: 'Jane', surname: 'Smith' }, + { id: 3, username: 'driver3', first_name: 'Bob', surname: 'Johnson' }, + { id: 4, username: 'driver4', first_name: 'Alice', surname: 'Williams' }, + { id: 5, username: 'driver5', first_name: 'Charlie', surname: 'Brown' }, + { id: 6, username: 'driver6', first_name: 'David', surname: 'Davis' }, + { id: 7, username: 'driver7', first_name: 'Eve', surname: 'Miller' }, + { id: 8, username: 'driver8', first_name: 'Frank', surname: 'Wilson' } + ]; + + const mockRaceResults: RaceResult[] = [ + { + id: 1, + track_id: 100, + id_1_place: 1, + id_2_place: 2, + id_3_place: 3, + id_4_place: 4, + id_5_place: 5, + id_6_place: 6, + id_7_place: 7, + id_8_place: 8, + id_fast_lap: 1, + list_dnf: '{7,8}' + }, + { + id: 2, + track_id: 101, + id_1_place: 2, + id_2_place: 1, + id_3_place: 4, + id_4_place: 3, + id_5_place: 6, + id_6_place: 5, + id_7_place: 8, + id_8_place: 7, + id_fast_lap: 2, + list_dnf: '{5}' + }, + { + id: 3, + track_id: 102, + id_1_place: 3, + id_2_place: 4, + id_3_place: 5, + id_4_place: 6, + id_5_place: 7, + id_6_place: 8, + id_7_place: 1, + id_8_place: 2, + id_fast_lap: 3, + list_dnf: '' + } + ]; + + const mockFantaVotes: FantaVote[] = [ + { + fanta_player_id: 1, + username: 'player1', + track_id: 100, + id_1_place: 1, + id_2_place: 2, + id_3_place: 3, + id_4_place: 4, + id_5_place: 5, + id_6_place: 6, + id_7_place: 7, + id_8_place: 8, + id_fast_lap: 1, + id_dnf: 7, + constructor_id: 1, + season_id: 1 + }, + { + fanta_player_id: 2, + username: 'player2', + track_id: 100, + id_1_place: 2, + id_2_place: 1, + id_3_place: 4, + id_4_place: 3, + id_5_place: 6, + id_6_place: 5, + id_7_place: 8, + id_8_place: 7, + id_fast_lap: 2, + id_dnf: 8, + constructor_id: 2, + season_id: 1 + }, + { + fanta_player_id: 1, + username: 'player1', + track_id: 101, + id_1_place: 2, + id_2_place: 1, + id_3_place: 3, + id_4_place: 4, + id_5_place: 5, + id_6_place: 6, + id_7_place: 7, + id_8_place: 8, + id_fast_lap: 2, + id_dnf: 5, + constructor_id: 1, + season_id: 1 + } + ]; + + const mockConstructorGrandPrixPoints: ConstructorGrandPrixPoints[] = [ + { + constructor_id: 1, + constructor_name: 'Team 1', + track_id: 100, + track_name: 'Track 1', + grand_prix_date: '2024-03-01T21:45:00.000Z', + grand_prix_id: 1, + season_id: 1, + season_description: 'Season 2024', + driver_id_1: 10, + driver_id_2: 11, + driver_1_points: 50, + driver_2_points: 50, + constructor_points: 100 + }, + { + constructor_id: 2, + constructor_name: 'Team 2', + track_id: 100, + track_name: 'Track 1', + grand_prix_date: '2024-03-01T21:45:00.000Z', + grand_prix_id: 1, + season_id: 1, + season_description: 'Season 2024', + driver_id_1: 12, + driver_id_2: 13, + driver_1_points: 40, + driver_2_points: 40, + constructor_points: 80 + }, + { + constructor_id: 1, + constructor_name: 'Team 1', + track_id: 101, + track_name: 'Track 2', + grand_prix_date: '2024-03-15T21:45:00.000Z', + grand_prix_id: 2, + season_id: 1, + season_description: 'Season 2024', + driver_id_1: 10, + driver_id_2: 11, + driver_1_points: 45, + driver_2_points: 45, + constructor_points: 90 + } + ]; + + beforeEach(async () => { + driversSignal = signal(mockDrivers); + constructorGrandPrixPointsSignal = signal(mockConstructorGrandPrixPoints); + raceResultSignal = signal(mockRaceResults); + mockDbDataService = { + drivers: driversSignal.asReadonly(), + constructorGrandPrixPoints: constructorGrandPrixPointsSignal.asReadonly(), + winningConstructorGrandPrixPoints: computed(() => constructorGrandPrixPointsSignal()), + raceResult: raceResultSignal.asReadonly() + }; + + mockApiService = jasmine.createSpyObj('ApiService', ['post']); + mockApiService.post.and.returnValue(of(mockFantaVotes)); + + TestBed.configureTestingModule({ + providers: [ + FantaService, + { provide: DbDataService, useValue: mockDbDataService as DbDataService }, + { provide: ApiService, useValue: mockApiService } + ] + }); + + service = TestBed.inject(FantaService); + await service.loadFantaVotes(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('initialization', () => { + it('should load fanta votes via API', () => { + expect(mockApiService.post).toHaveBeenCalledWith('/fanta/votes', {}); + }); + + it('should calculate points after loading', () => { + expect(service.getFantaPoints(1)).toBeGreaterThan(0); + }); + }); + + describe('getFantaPoints', () => { + it('should return points for valid user', () => { + const points = service.getFantaPoints(1); + expect(points).toBeGreaterThan(0); + }); + + it('should return 0 for user with no votes', () => { + const points = service.getFantaPoints(999); + expect(points).toBe(0); + }); + }); + + describe('getFantaNumberVotes', () => { + it('should return correct number of votes for user', () => { + const votes = service.getFantaNumberVotes(1); + expect(votes).toBe(2); + }); + + it('should return 0 for user with no votes', () => { + const votes = service.getFantaNumberVotes(999); + expect(votes).toBe(0); + }); + }); + + describe('getFantaRacePoints', () => { + it('should return points for specific race and user', () => { + const points = service.getFantaRacePoints(1, 100); + expect(points).toBeGreaterThan(0); + }); + + it('should return 0 for non-existent race', () => { + const points = service.getFantaRacePoints(1, 999); + expect(points).toBe(0); + }); + + it('should return 0 for non-existent user', () => { + const points = service.getFantaRacePoints(999, 100); + expect(points).toBe(0); + }); + }); + + describe('getFantaRacePointsBreakdown', () => { + it('should return breakdown for user with votes', () => { + const breakdown = service.getFantaRacePointsBreakdown(1); + expect(breakdown.length).toBe(2); + expect(breakdown[0].raceId).toBe(100); + expect(breakdown[1].raceId).toBe(101); + }); + + it('should return empty array for user with no votes', () => { + const breakdown = service.getFantaRacePointsBreakdown(999); + expect(breakdown).toEqual([]); + }); + + it('should sort breakdown by race ID', () => { + const breakdown = service.getFantaRacePointsBreakdown(1); + for (let i = 1; i < breakdown.length; i++) { + expect(breakdown[i].raceId).toBeGreaterThan(breakdown[i - 1].raceId); + } + }); + }); + + describe('totNumberVotes', () => { + it('should return total number of races', () => { + expect(service.totNumberVotes()).toBe(mockRaceResults.length); + }); + }); + + describe('getFantaVote', () => { + it('should return vote for valid user and race', () => { + const vote = service.getFantaVote(1, 100); + expect(vote).toBeDefined(); + expect(vote?.fanta_player_id).toBe(1); + expect(vote?.track_id).toBe(100); + }); + + it('should return undefined for non-existent vote', () => { + const vote = service.getFantaVote(999, 100); + expect(vote).toBeUndefined(); + }); + }); + + describe('getRaceResult', () => { + it('should return race result for valid race ID', () => { + const result = service.getRaceResult(100); + expect(result).toBeDefined(); + expect(result?.track_id).toBe(100); + }); + + it('should return undefined for non-existent race', () => { + const result = service.getRaceResult(999); + expect(result).toBeUndefined(); + }); + }); + + describe('getWinningConstructorsForTrack', () => { + it('should return winning constructor ID for valid track', () => { + const constructorIds = service.getWinningConstructorsForTrack(100); + expect(constructorIds).toEqual([1]); + }); + + it('should return empty array for track with no data', () => { + const constructorIds = service.getWinningConstructorsForTrack(999); + expect(constructorIds).toEqual([]); + }); + + it('should return all constructors when multiple constructors tie for first', () => { + // Create mock data with two constructors tied at 100 points + const tiedConstructors: ConstructorGrandPrixPoints[] = [ + { + constructor_id: 1, + constructor_name: 'Team 1', + track_id: 103, + track_name: 'Track 3', + grand_prix_date: '2024-04-01T21:45:00.000Z', + grand_prix_id: 3, + season_id: 1, + season_description: 'Season 2024', + driver_id_1: 10, + driver_id_2: 11, + driver_1_points: 50, + driver_2_points: 50, + constructor_points: 100 + }, + { + constructor_id: 2, + constructor_name: 'Team 2', + track_id: 103, + track_name: 'Track 3', + grand_prix_date: '2024-04-01T21:45:00.000Z', + grand_prix_id: 3, + season_id: 1, + season_description: 'Season 2024', + driver_id_1: 12, + driver_id_2: 13, + driver_1_points: 50, + driver_2_points: 50, + constructor_points: 100 + }, + { + constructor_id: 3, + constructor_name: 'Team 3', + track_id: 103, + track_name: 'Track 3', + grand_prix_date: '2024-04-01T21:45:00.000Z', + grand_prix_id: 3, + season_id: 1, + season_description: 'Season 2024', + driver_id_1: 14, + driver_id_2: 15, + driver_1_points: 40, + driver_2_points: 40, + constructor_points: 80 + } + ]; + + constructorGrandPrixPointsSignal.set([...mockConstructorGrandPrixPoints, ...tiedConstructors]); + + const constructorIds = service.getWinningConstructorsForTrack(103); + expect(constructorIds).toEqual([1, 2]); + expect(constructorIds.length).toBe(2); + }); + }); + + describe('calculateFantaPoints', () => { + it('should calculate points for perfect prediction', () => { + const raceResult = mockRaceResults[0]; + const fantaVote = mockFantaVotes[0]; + const points = service.calculateFantaPoints(raceResult, fantaVote); + + // Perfect prediction: 8 drivers * 7 points + fast lap (5) + dnf (5) + team (5) = 71 + expect(points).toBeGreaterThan(0); + }); + + it('should award points for correct fast lap prediction', () => { + const raceResult: RaceResult = { + id: 1, + track_id: 100, + id_1_place: 1, + id_2_place: 2, + id_3_place: 3, + id_4_place: 4, + id_5_place: 5, + id_6_place: 6, + id_7_place: 7, + id_8_place: 8, + id_fast_lap: 1, + list_dnf: '' + }; + + const fantaVote: FantaVote = { + fanta_player_id: 1, + username: 'player1', + track_id: 100, + id_1_place: 2, + id_2_place: 1, + id_3_place: 3, + id_4_place: 4, + id_5_place: 5, + id_6_place: 6, + id_7_place: 7, + id_8_place: 8, + id_fast_lap: 1, + id_dnf: 0, + constructor_id: 0 + }; + + const points = service.calculateFantaPoints(raceResult, fantaVote); + expect(points).toBeGreaterThanOrEqual(FantaService.CORRECT_RESPONSE_FAST_LAP_POINTS); + }); + + it('should award points for correct DNF prediction', () => { + const raceResult: RaceResult = { + id: 1, + track_id: 100, + id_1_place: 1, + id_2_place: 2, + id_3_place: 3, + id_4_place: 4, + id_5_place: 5, + id_6_place: 6, + id_7_place: 7, + id_8_place: 8, + id_fast_lap: 0, + list_dnf: '{7}' + }; + + const fantaVote: FantaVote = { + fanta_player_id: 1, + username: 'player1', + track_id: 100, + id_1_place: 1, + id_2_place: 2, + id_3_place: 3, + id_4_place: 4, + id_5_place: 5, + id_6_place: 6, + id_7_place: 7, + id_8_place: 8, + id_fast_lap: 0, + id_dnf: 7, + constructor_id: 0 + }; + + const points = service.calculateFantaPoints(raceResult, fantaVote); + expect(points).toBeGreaterThanOrEqual(FantaService.CORRECT_RESPONSE_DNF_POINTS); + }); + + it('should not award fast lap points when prediction is wrong', () => { + const raceResult: RaceResult = { + id: 1, + track_id: 100, + id_1_place: 1, + id_2_place: 2, + id_3_place: 3, + id_4_place: 4, + id_5_place: 5, + id_6_place: 6, + id_7_place: 7, + id_8_place: 8, + id_fast_lap: 1, + list_dnf: '' + }; + + const fantaVote: FantaVote = { + fanta_player_id: 1, + username: 'player1', + track_id: 100, + id_1_place: 1, + id_2_place: 2, + id_3_place: 3, + id_4_place: 4, + id_5_place: 5, + id_6_place: 6, + id_7_place: 7, + id_8_place: 8, + id_fast_lap: 2, + id_dnf: 0, + constructor_id: 0 + }; + + const pointsWithWrongFastLap = service.calculateFantaPoints(raceResult, fantaVote); + + fantaVote.id_fast_lap = 1; + const pointsWithCorrectFastLap = service.calculateFantaPoints(raceResult, fantaVote); + + expect(pointsWithCorrectFastLap).toBe(pointsWithWrongFastLap + FantaService.CORRECT_RESPONSE_FAST_LAP_POINTS); + }); + + it('should award constructor points when user picks any tied constructor', () => { + // Setup tied constructors for track 103 + const tiedConstructors: ConstructorGrandPrixPoints[] = [ + { + constructor_id: 1, + constructor_name: 'Team 1', + track_id: 103, + track_name: 'Track 3', + grand_prix_date: '2024-04-01T21:45:00.000Z', + grand_prix_id: 3, + season_id: 1, + season_description: 'Season 2024', + driver_id_1: 10, + driver_id_2: 11, + driver_1_points: 50, + driver_2_points: 50, + constructor_points: 100 + }, + { + constructor_id: 2, + constructor_name: 'Team 2', + track_id: 103, + track_name: 'Track 3', + grand_prix_date: '2024-04-01T21:45:00.000Z', + grand_prix_id: 3, + season_id: 1, + season_description: 'Season 2024', + driver_id_1: 12, + driver_id_2: 13, + driver_1_points: 50, + driver_2_points: 50, + constructor_points: 100 + } + ]; + + constructorGrandPrixPointsSignal.set([...mockConstructorGrandPrixPoints, ...tiedConstructors]); + + const raceResult: RaceResult = { + id: 3, + track_id: 103, + id_1_place: 1, + id_2_place: 2, + id_3_place: 3, + id_4_place: 4, + id_5_place: 5, + id_6_place: 6, + id_7_place: 7, + id_8_place: 8, + id_fast_lap: 0, + list_dnf: '' + }; + + // Test voting for constructor 1 (tied winner) + const fantaVote1: FantaVote = { + fanta_player_id: 1, + username: 'player1', + track_id: 103, + id_1_place: 1, + id_2_place: 2, + id_3_place: 3, + id_4_place: 4, + id_5_place: 5, + id_6_place: 6, + id_7_place: 7, + id_8_place: 8, + id_fast_lap: 0, + id_dnf: 0, + constructor_id: 1 + }; + + // Test voting for constructor 2 (also tied winner) + const fantaVote2: FantaVote = { + ...fantaVote1, + constructor_id: 2 + }; + + const pointsConstructor1 = service.calculateFantaPoints(raceResult, fantaVote1); + const pointsConstructor2 = service.calculateFantaPoints(raceResult, fantaVote2); + + // Both should get the same points (including constructor bonus) + expect(pointsConstructor1).toBe(pointsConstructor2); + expect(pointsConstructor1).toBeGreaterThanOrEqual(FantaService.CORRECT_RESPONSE_TEAM); + }); + }); + + describe('isDnfCorrect', () => { + it('should return true when DNF prediction is correct', () => { + expect(service.isDnfCorrect('{7,8}', 7)).toBe(true); + expect(service.isDnfCorrect('{7,8}', 8)).toBe(true); + }); + + it('should return false when DNF prediction is wrong', () => { + expect(service.isDnfCorrect('{7,8}', 1)).toBe(false); + }); + + it('should return false for empty DNF list', () => { + expect(service.isDnfCorrect('', 7)).toBe(false); + }); + + it('should return false for null/undefined values', () => { + expect(service.isDnfCorrect('', 0)).toBe(false); + }); + + it('should handle DNF list with spaces', () => { + expect(service.isDnfCorrect('{7, 8}', 7)).toBe(true); + }); + }); + + describe('pointsWithAbsoluteDifference', () => { + it('should return 7 points for exact match', () => { + expect(service.pointsWithAbsoluteDifference(0, 0)).toBe(7); + }); + + it('should return 4 points for difference of 1', () => { + expect(service.pointsWithAbsoluteDifference(0, 1)).toBe(4); + expect(service.pointsWithAbsoluteDifference(1, 0)).toBe(4); + }); + + it('should return 2 points for difference of 2', () => { + expect(service.pointsWithAbsoluteDifference(0, 2)).toBe(2); + expect(service.pointsWithAbsoluteDifference(2, 0)).toBe(2); + }); + + it('should return 0 points for difference greater than 2', () => { + expect(service.pointsWithAbsoluteDifference(0, 3)).toBe(0); + expect(service.pointsWithAbsoluteDifference(0, 7)).toBe(0); + }); + }); + + describe('edge cases', () => { + it('should handle race results with invalid positions', () => { + const incompleteRaceResult: RaceResult = { + id: 99, + track_id: 999, + id_1_place: 0, + id_2_place: 0, + id_3_place: 0, + id_4_place: 0, + id_5_place: 0, + id_6_place: 0, + id_7_place: 0, + id_8_place: 0, + id_fast_lap: 0, + list_dnf: '' + }; + + const extendedResults = [...mockRaceResults, incompleteRaceResult]; + raceResultSignal.set(extendedResults); + + expect(service.totNumberVotes()).toBe(mockRaceResults.length + 1); + }); + + it('should handle empty fanta votes', async () => { + mockApiService.post.and.returnValue(of([])); + await service.loadFantaVotes(); + + expect(service.getFantaPoints(1)).toBe(0); + expect(service.getFantaNumberVotes(1)).toBe(0); + }); + + it('should handle empty race results', () => { + raceResultSignal.set([]); + + // Reconfigure TestBed with empty data + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + FantaService, + { provide: DbDataService, useValue: mockDbDataService as DbDataService }, + { provide: ApiService, useValue: mockApiService } + ] + }); + + const newService = TestBed.inject(FantaService); + expect(newService.totNumberVotes()).toBe(0); + }); + }); +}); + + diff --git a/client/src/app/service/fanta.service.ts b/client/src/app/service/fanta.service.ts new file mode 100644 index 000000000..b36bb09cf --- /dev/null +++ b/client/src/app/service/fanta.service.ts @@ -0,0 +1,193 @@ +import { Injectable, inject, signal, computed } from '@angular/core'; +import { DbDataService } from './db-data.service'; +import { ApiService } from './api.service'; +import { size } from 'lodash-es'; +import { firstValueFrom } from 'rxjs'; +import type { FantaVote, RaceResult } from '@f123dashboard/shared'; + +@Injectable({ + providedIn: 'root' +}) +export class FantaService { + public static readonly CORRECT_RESPONSE_FAST_LAP_POINTS: number = 5; + public static readonly CORRECT_RESPONSE_DNF_POINTS: number = 5; + public static readonly CORRECT_RESPONSE_TEAM: number = 5; + public static readonly CORRECT_RESPONSE_POINTS: Readonly> = { + 0: 7, + 1: 4, + 2: 2 + }; + private dbData = inject(DbDataService); + private apiService = inject(ApiService); + + private fantaVotesSignal = signal([]); + readonly fantaVotes = this.fantaVotesSignal.asReadonly(); + + readonly raceResults = computed(() => + this.dbData.raceResult().filter(result => result.id_1_place !== null) + ); + + private readonly _pointsCache = computed(() => { + const fantaPoints = new Map(); + const fantaNumberVotes = new Map(); + const fantaRacePoints = new Map(); + + const votes = this.fantaVotes(); + const results = this.raceResults(); + + results.forEach(raceResult => { + const raceVotes = votes.filter(item => item.track_id === raceResult.track_id && item.id_1_place !== null); + if (raceVotes.length === 0) { return; } + + raceVotes.forEach(raceVote => { + const racePoints = this.calculateFantaPoints(raceResult, raceVote); + const playerId = Number(raceVote.fanta_player_id); + const raceId = Number(raceResult.track_id); + + const raceKey = `${playerId}_${raceId}`; + fantaRacePoints.set(raceKey, racePoints); + + fantaNumberVotes.set(playerId, (fantaNumberVotes.get(playerId) ?? 0) + 1); + fantaPoints.set(playerId, (fantaPoints.get(playerId) ?? 0) + racePoints); + }); + }); + + return { + fantaPoints: new Map(Array.from(fantaPoints.entries()).sort(([, a], [, b]) => a - b)), + fantaNumberVotes, + fantaRacePoints + }; + }); + + readonly fantaPoints = computed(() => this._pointsCache().fantaPoints); + readonly fantaNumberVotes = computed(() => this._pointsCache().fantaNumberVotes); + readonly fantaRacePoints = computed(() => this._pointsCache().fantaRacePoints); + + + async loadFantaVotes(): Promise { + const fantaVotes = await firstValueFrom( + this.apiService.post('/fanta/votes', {}) + ); + this.fantaVotesSignal.set(fantaVotes); + + } + + async setFantaVote(voto: FantaVote): Promise { + await firstValueFrom( + this.apiService.post('/fanta/set-vote', voto) + ); + + await this.loadFantaVotes(); + } + + getFantaPoints(userId: number): number { + return this.fantaPoints().get(Number(userId)) ?? 0; + } + + getFantaNumberVotes(userId: number): number { + return this.fantaNumberVotes().get(Number(userId)) ?? 0; + } + + getFantaRacePoints(userId: number, raceId: number): number { + const raceKey = `${userId}_${raceId}`; + return this.fantaRacePoints().get(raceKey) ?? 0; + } + + getFantaRacePointsBreakdown(userId: number): {raceId: number, points: number}[] { + const breakdown: {raceId: number, points: number}[] = []; + + this.fantaRacePoints().forEach((points, key) => { + const [playerId, raceId] = key.split('_').map(Number); + if (playerId === userId) { + breakdown.push({ raceId, points }); + } + }); + + return breakdown.sort((a, b) => a.raceId - b.raceId); + } + + readonly totNumberVotes = computed(() => size(this.raceResults())); + + getFantaVote(userId: number, raceId: number): FantaVote | undefined { + return this.fantaVotes() + .filter(vote => vote.fanta_player_id === userId) + .find(vote => vote.track_id === raceId); + } + + getRaceResult(raceId: number): RaceResult | undefined { + return this.raceResults().find(race => race.track_id === raceId); + } + + /** + * Get the winning constructor(s) for a specific track/race. + * Returns all constructors that tied for first place with the highest points. + */ + getWinningConstructorsForTrack(trackId: number): number[] { + const allConstructorGpPoints = this.dbData.constructorGrandPrixPoints(); + const constructorsForTrack = allConstructorGpPoints.filter(c => +c.track_id === +trackId); + + if (constructorsForTrack.length === 0) { + return []; + } + + // Find the maximum points for this track + const maxPoints = Math.max(...constructorsForTrack.map(c => +c.constructor_points)); + + // Return all constructors that have the maximum points (handles ties) + return constructorsForTrack + .filter(c => +c.constructor_points === maxPoints) + .map(c => +c.constructor_id) + .filter(id => id !== null && id !== undefined); + } + + calculateFantaPoints(raceResult: RaceResult, fantaVote: FantaVote): number { + let points = 0; + // Create arrays of positions for both result and vote + const resultPositions = [ + Number(raceResult.id_1_place), Number(raceResult.id_2_place), Number(raceResult.id_3_place), Number(raceResult.id_4_place), + Number(raceResult.id_5_place), Number(raceResult.id_6_place), Number(raceResult.id_7_place), Number(raceResult.id_8_place) + ]; + + const votePositions = [ + Number(fantaVote.id_1_place), Number(fantaVote.id_2_place), Number(fantaVote.id_3_place), Number(fantaVote.id_4_place), + Number(fantaVote.id_5_place), Number(fantaVote.id_6_place), Number(fantaVote.id_7_place), Number(fantaVote.id_8_place) + ]; + // Calculate points for each driver (1-8) + this.dbData.drivers().forEach(driver => { + const realPosition = resultPositions.indexOf(Number(driver.id)); + const votedPosition = votePositions.indexOf(Number(driver.id)); + if (votedPosition === -1 || realPosition === -1) {return;} + + points += this.pointsWithAbsoluteDifference(realPosition, votedPosition); + }); + + // Calculate points for Fast Lap and DNF + points = (raceResult.id_fast_lap === fantaVote.id_fast_lap && fantaVote.id_fast_lap !== 0) ? points + FantaService.CORRECT_RESPONSE_FAST_LAP_POINTS : points; + points = (this.isDnfCorrect(raceResult.list_dnf, fantaVote.id_dnf) && fantaVote.id_dnf !== 0) ? points + FantaService.CORRECT_RESPONSE_DNF_POINTS : points; + + // Calculate points for Constructor Team (all tied constructors with highest points count as winners) + const winningConstructorIds = this.getWinningConstructorsForTrack(+raceResult.track_id); + const isConstructorWinner = winningConstructorIds.includes(fantaVote.constructor_id) && fantaVote.constructor_id !== 0; + points = isConstructorWinner ? points + FantaService.CORRECT_RESPONSE_TEAM : points; + + return points; + } + + isDnfCorrect(raceResultDnf: string, fantaVoteDnfId: number) { + //let fantaVoteDnfUsername: string = this.allDrivers.find(driver => driver.driver_id == fantaVoteDnfId)?.driver_username; + //return raceResultDnf.includes(fantaVoteDnfUsername) ; + // now list_dnf contains the driver_id, so we can check directly + if (!raceResultDnf || !fantaVoteDnfId) {return false;} + // raceResultDnf is a string like "{1,4}", so extract numbers + const dnfStr = typeof raceResultDnf === 'string' ? raceResultDnf : String(raceResultDnf ?? ''); + const ids = dnfStr.replace(/[{}]/g, '').split(',').map(id => Number(id.trim())).filter(id => !isNaN(id)); + return ids.includes(+fantaVoteDnfId); + + } + + pointsWithAbsoluteDifference(raceResult: number, fantaVote: number) : number{ + const absDiff = Math.abs(raceResult - fantaVote); + return FantaService.CORRECT_RESPONSE_POINTS[absDiff] ?? 0; + } + +} diff --git a/client/src/app/service/gp-edit.service.ts b/client/src/app/service/gp-edit.service.ts new file mode 100644 index 000000000..161f5af24 --- /dev/null +++ b/client/src/app/service/gp-edit.service.ts @@ -0,0 +1,36 @@ +import { Injectable, inject } from '@angular/core'; +import { Observable } from 'rxjs'; +import { ApiService } from './api.service'; +import type { GPEditItem, CreateGpData, UpdateGpData } from '@f123dashboard/shared'; + +@Injectable({ + providedIn: 'root' +}) +export class GpEditService { + private api = inject(ApiService); + private readonly baseUrl = 'gp-edit'; + + getUpcomingGps(): Observable<{ success: boolean; data: GPEditItem[] }> { + return this.api.post<{ success: boolean; data: GPEditItem[] }>(`${this.baseUrl}/list`, {}); + } + + getAllTracks(): Observable<{ success: boolean; data: {id: number, name: string}[] }> { + return this.api.post<{ success: boolean; data: {id: number, name: string}[] }>(`${this.baseUrl}/tracks`, {}); + } + + createGp(data: CreateGpData): Observable<{ success: boolean; data: GPEditItem }> { + return this.api.post<{ success: boolean; data: GPEditItem }>(`${this.baseUrl}/create`, data); + } + + updateGp(id: number, data: UpdateGpData): Observable<{ success: boolean }> { + return this.api.post<{ success: boolean }>(`${this.baseUrl}/update/${id}`, data); + } + + deleteGp(id: number): Observable<{ success: boolean }> { + return this.api.post<{ success: boolean }>(`${this.baseUrl}/delete/${id}`, {}); + } + + bulkUpdateGpDate(daysOffset: number): Observable<{ success: boolean }> { + return this.api.post<{ success: boolean }>(`${this.baseUrl}/bulk-update-date`, { daysOffset }); + } +} diff --git a/client/src/app/service/loading.service.spec.ts b/client/src/app/service/loading.service.spec.ts new file mode 100644 index 000000000..21a5381ef --- /dev/null +++ b/client/src/app/service/loading.service.spec.ts @@ -0,0 +1,37 @@ +import { TestBed } from '@angular/core/testing'; +import { LoadingService } from './loading.service'; + +describe('LoadingService', () => { + let service: LoadingService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(LoadingService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should show loading on first show() call', () => { + expect(service.loading()).toBeFalse(); + service.show(); + expect(service.loading()).toBeTrue(); + }); + + it('should hide loading after matching hide() calls', () => { + service.show(); + service.show(); + expect(service.loading()).toBeTrue(); + service.hide(); + expect(service.loading()).toBeTrue(); + service.hide(); + expect(service.loading()).toBeFalse(); + }); + + it('should not affect loading state when hide() is called without show()', () => { + expect(service.loading()).toBeFalse(); + service.hide(); + expect(service.loading()).toBeFalse(); + }); +}); \ No newline at end of file diff --git a/client/src/app/service/loading.service.ts b/client/src/app/service/loading.service.ts new file mode 100644 index 000000000..205db8d9e --- /dev/null +++ b/client/src/app/service/loading.service.ts @@ -0,0 +1,18 @@ +import { Injectable, signal } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class LoadingService { + private loadingSignal = signal(false); + private callNumber = 0; + public readonly loading = this.loadingSignal.asReadonly(); + + show() { + if (this.callNumber === 0) {this.loadingSignal.set(true);} + this.callNumber++; + } + + hide() { + if(this.callNumber > 0) {this.callNumber--;} + if(this.callNumber === 0) {this.loadingSignal.set(false);} + } +} \ No newline at end of file diff --git a/client/src/app/service/playground.service.spec.ts b/client/src/app/service/playground.service.spec.ts new file mode 100644 index 000000000..e6437ccd3 --- /dev/null +++ b/client/src/app/service/playground.service.spec.ts @@ -0,0 +1,19 @@ +import { TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; + +import { PlaygroundService } from './playground.service'; + +describe('DbDataService', () => { + let service: PlaygroundService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [provideNoopAnimations(), ],}); + service = TestBed.inject(PlaygroundService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/client/src/app/service/playground.service.ts b/client/src/app/service/playground.service.ts new file mode 100644 index 000000000..5c5822e40 --- /dev/null +++ b/client/src/app/service/playground.service.ts @@ -0,0 +1,62 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import type { PlaygroundBestScore } from '@f123dashboard/shared'; +import { ApiService } from './api.service'; + +@Injectable({ + providedIn: 'root' +}) +export class PlaygroundService { + private apiService = inject(ApiService); + +/****************************************************************/ +//variabili locali +/****************************************************************/ + private playgroundLeaderboardSignal = signal([]); + + // Readonly signal for external access + public playgroundLeaderboard = this.playgroundLeaderboardSignal.asReadonly(); + + +/****************************************************************/ +//compilazione delle variabili pre caricamento del interfaccia web +/****************************************************************/ + + async allData() { + const [ + playgroundLeaderboard + ] = await Promise.all([ + firstValueFrom(this.apiService.post('/playground/leaderboard', {})) + ]); + + this.playgroundLeaderboardSignal.set(playgroundLeaderboard); + } + + getUserBestScore(userId: number): number { + const userScore = this.playgroundLeaderboardSignal().find(score => score.user_id === userId); + return userScore ? userScore.best_score : 9999; // return 9999 if no score found + } + + async setUserBestScore(voto: PlaygroundBestScore): Promise { + await firstValueFrom( + this.apiService.post('/playground/score', voto) + ); + + // Update the signal with the new score + this.playgroundLeaderboardSignal.update(leaderboard => { + const tmp = [...leaderboard]; + const foundIndex = tmp.findIndex(score => score.user_id === voto.user_id); + if (foundIndex === -1) { + // New entry + tmp.push(voto); + } else { + // Update existing entry + tmp[foundIndex] = voto; + } + // Sort by best score + return tmp.sort((a, b) => a.best_score - b.best_score); + }); + } +} + +export { PlaygroundBestScore }; diff --git a/client/src/app/service/season.service.ts b/client/src/app/service/season.service.ts new file mode 100644 index 000000000..4f1dcdf4e --- /dev/null +++ b/client/src/app/service/season.service.ts @@ -0,0 +1,44 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import type { Season } from '@f123dashboard/shared'; +import { ApiService } from './api.service'; + +@Injectable({ providedIn: 'root' }) +export class SeasonService { + private apiService = inject(ApiService); + private allSesons: Season[] = []; + private currentSeasonSignal = signal(null!); + public readonly season = this.currentSeasonSignal.asReadonly(); + + + setCurreentSeason(season: Season) { + this.currentSeasonSignal.set(season); + } + getCurrentSeason(): Season { + return this.currentSeasonSignal(); + } + + public getAllSeasons(): Season[] { + return this.allSesons; + } + + public setAllSeasons() { + firstValueFrom( + this.apiService.post('/database/seasons', {}) + ).then((seasons: Season[]) => { + if (seasons.length === 0) { + console.error("No seasons found in the database."); + return; + } + this.allSesons = seasons; + const latestSeason = this.allSesons.reduce((latest, current) => + latest?.startDate && current?.startDate && latest.startDate > current.startDate ? latest : current + , this.allSesons[0] + ); + this.setCurreentSeason(latestSeason); + }).catch((error: unknown) => { + console.error("Error fetching seasons:", error); + }); + } + +} \ No newline at end of file diff --git a/client/src/app/service/twitch-api.service.ts b/client/src/app/service/twitch-api.service.ts new file mode 100644 index 000000000..8bb77a6f2 --- /dev/null +++ b/client/src/app/service/twitch-api.service.ts @@ -0,0 +1,27 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import type { TwitchStreamResponse } from '@f123dashboard/shared'; +import { ApiService } from './api.service'; + +@Injectable({ + providedIn: 'root', +}) +export class TwitchApiService { + private apiService = inject(ApiService); + private channelId = 'dreandos'; + private isLiveSignal = signal(false); + public readonly isLive = this.isLiveSignal.asReadonly(); + private twitchStreamResponse: TwitchStreamResponse |null = null; + + async checkStreamStatus(){ + this.twitchStreamResponse = await firstValueFrom( + this.apiService.post('/twitch/stream-info', { channelName: this.channelId }) + ); + this.isLiveSignal.set(this.twitchStreamResponse && this.twitchStreamResponse.data.length > 0); + } + + getChannel(): string { + return this.channelId; + } + +} \ No newline at end of file diff --git a/client/src/app/views/admin-change-password/admin-change-password.component.html b/client/src/app/views/admin-change-password/admin-change-password.component.html new file mode 100644 index 000000000..eab8e780d --- /dev/null +++ b/client/src/app/views/admin-change-password/admin-change-password.component.html @@ -0,0 +1,123 @@ + + + + + +

+ + Modifica Password Utente +

+
+ + + @if (showSuccess()) { + + + {{ successMessage() }} +
+ Password Temporanea: +
+ {{ generatedPassword() }} + +
+ + ⚠️ Assicurati di comunicare questa password all'utente in modo sicuro. Sarà inviata anche via email. + +
+
+ } + + + @if (showError()) { + + + {{ errorMessage() }} + + } + +
+ + +
+ + + @if (isFieldInvalid('username')) { +
+ {{ getErrorMessage('username') }} +
+ } + + Inserisci il nome utente dell'account di cui vuoi modificare la password. Verrà generata automaticamente una password temporanea. + +
+ + +
+ +
+
+
+
+ + + +
+
+
diff --git a/client/src/app/views/admin-change-password/admin-change-password.component.scss b/client/src/app/views/admin-change-password/admin-change-password.component.scss new file mode 100644 index 000000000..f73d9562a --- /dev/null +++ b/client/src/app/views/admin-change-password/admin-change-password.component.scss @@ -0,0 +1,150 @@ +.admin-change-password-container { + padding-top: 2rem; + padding-bottom: 2rem; + min-height: calc(100vh - 200px); + + c-card { + border-radius: 0.5rem; + border: none; + + c-card-header { + border-top-left-radius: 0.5rem; + border-top-right-radius: 0.5rem; + padding: 1.5rem; + + h4 { + font-weight: 600; + display: flex; + align-items: center; + } + + .small { + opacity: 0.9; + } + } + + c-card-body { + padding: 2rem; + } + } + + .form-label { + font-weight: 500; + color: var(--cui-body-color); + margin-bottom: 0.5rem; + display: flex; + align-items: center; + } + + input[cFormControl] { + border-radius: 0.375rem; + padding: 0.75rem 1rem; + font-size: 1rem; + border: 1px solid var(--cui-border-color); + transition: all 0.2s ease-in-out; + + &:focus { + border-color: var(--cui-primary); + box-shadow: 0 0 0 0.25rem rgba(var(--cui-primary-rgb), 0.25); + } + + &.is-invalid { + border-color: var(--cui-danger); + + &:focus { + border-color: var(--cui-danger); + box-shadow: 0 0 0 0.25rem rgba(var(--cui-danger-rgb), 0.25); + } + } + } + + .invalid-feedback { + display: block; + margin-top: 0.25rem; + font-size: 0.875rem; + color: var(--cui-danger); + } + + .form-text { + margin-top: 0.25rem; + font-size: 0.875rem; + } + + .alert { + border-radius: 0.375rem; + + &.alert-warning { + background-color: rgba(var(--cui-warning-rgb), 0.1); + border-color: var(--cui-warning); + color: var(--cui-warning-text-emphasis); + } + + &.alert-info { + background-color: rgba(var(--cui-info-rgb), 0.1); + border-color: var(--cui-info); + color: var(--cui-info-text-emphasis); + + .alert-heading { + font-weight: 600; + margin-bottom: 0.5rem; + display: flex; + align-items: center; + } + } + } + + button[cButton] { + border-radius: 0.375rem; + font-weight: 500; + transition: all 0.2s ease-in-out; + + &:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + } + + c-alert { + border-radius: 0.375rem; + margin-bottom: 1.5rem; + + &[color="success"] { + background-color: rgba(var(--cui-success-rgb), 0.1); + border-color: var(--cui-success); + color: var(--cui-success-text-emphasis); + } + + &[color="danger"] { + background-color: rgba(var(--cui-danger-rgb), 0.1); + border-color: var(--cui-danger); + color: var(--cui-danger-text-emphasis); + } + } +} + +// Responsive adjustments +@media (max-width: 768px) { + .admin-change-password-container { + padding-top: 1rem; + padding-bottom: 1rem; + + c-card { + c-card-header { + padding: 1rem; + + h4 { + font-size: 1.25rem; + } + } + + c-card-body { + padding: 1.5rem; + } + } + } +} diff --git a/client/src/app/views/admin-change-password/admin-change-password.component.spec.ts b/client/src/app/views/admin-change-password/admin-change-password.component.spec.ts new file mode 100644 index 000000000..58630ebc8 --- /dev/null +++ b/client/src/app/views/admin-change-password/admin-change-password.component.spec.ts @@ -0,0 +1,70 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { signal } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; +import { AdminChangePasswordComponent } from './admin-change-password.component'; +import { AuthService } from 'src/app/service/auth.service'; + +describe('AdminChangePasswordComponent', () => { + let component: AdminChangePasswordComponent; + let fixture: ComponentFixture; + let mockAuthService: jasmine.SpyObj; + let mockRouter: jasmine.SpyObj; + + beforeEach(async () => { + mockAuthService = jasmine.createSpyObj('AuthService', ['getToken'], { currentUser: signal({ id: 1, username: 'test', name: 'Test', surname: 'User', isAdmin: false }) }); + mockRouter = jasmine.createSpyObj('Router', ['navigate']); + + await TestBed.configureTestingModule({ + imports: [AdminChangePasswordComponent, ReactiveFormsModule], + providers: [provideNoopAnimations(), { provide: AuthService, useValue: mockAuthService }, + { provide: Router, useValue: mockRouter } + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AdminChangePasswordComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should redirect non-admin users', async () => { + TestBed.resetTestingModule(); + const nonAdminAuthService = jasmine.createSpyObj('AuthService', ['getToken'], { currentUser: signal({ id: 1, username: 'test', name: 'Test', surname: 'User', isAdmin: false }) }); + const testRouter = jasmine.createSpyObj('Router', ['navigate']); + + await TestBed.configureTestingModule({ + imports: [AdminChangePasswordComponent, ReactiveFormsModule], + providers: [ + provideNoopAnimations(), + { provide: AuthService, useValue: nonAdminAuthService }, + { provide: Router, useValue: testRouter } + ] + }).compileComponents(); + + const testFixture = TestBed.createComponent(AdminChangePasswordComponent); + testFixture.detectChanges(); + await testFixture.whenStable(); + expect(testRouter.navigate).toHaveBeenCalledWith(['/dashboard']); + }); + + it('should initialize form with empty values', () => { + fixture.detectChanges(); + expect(component.changePasswordForm.get('username')?.value).toBe(''); + }); + + it('should validate username is required', () => { + fixture.detectChanges(); + const form = component.changePasswordForm; + const usernameControl = form.get('username'); + + expect(usernameControl?.hasError('required')).toBeTruthy(); + + usernameControl?.setValue('user'); + expect(usernameControl?.hasError('required')).toBeFalsy(); + }); +}); diff --git a/client/src/app/views/admin-change-password/admin-change-password.component.ts b/client/src/app/views/admin-change-password/admin-change-password.component.ts new file mode 100644 index 000000000..8911b0f89 --- /dev/null +++ b/client/src/app/views/admin-change-password/admin-change-password.component.ts @@ -0,0 +1,180 @@ +import { Component, OnInit, inject, signal, ChangeDetectionStrategy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { + ContainerComponent, + RowComponent, + ColComponent, + CardComponent, + CardBodyComponent, + CardHeaderComponent, + FormDirective, + FormControlDirective, + ButtonDirective, + SpinnerComponent, + AlertComponent +} from '@coreui/angular'; +import { IconDirective } from '@coreui/icons-angular'; +import { cilLockLocked, cilUser, cilCheckCircle, cilXCircle } from '@coreui/icons'; +import type { ChangePasswordResponse } from '@f123dashboard/shared'; + +import { AuthService } from 'src/app/service/auth.service'; +import { ApiService } from 'src/app/service/api.service'; +import { firstValueFrom } from 'rxjs'; + +@Component({ + selector: 'app-admin-change-password', + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + ContainerComponent, + RowComponent, + ColComponent, + CardComponent, + CardBodyComponent, + CardHeaderComponent, + FormDirective, + FormControlDirective, + ButtonDirective, + SpinnerComponent, + AlertComponent, + IconDirective + ], + templateUrl: './admin-change-password.component.html', + styleUrl: './admin-change-password.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AdminChangePasswordComponent implements OnInit { + private fb = inject(FormBuilder); + private authService = inject(AuthService); + private router = inject(Router); + private apiService = inject(ApiService); + + changePasswordForm: FormGroup; + isLoading = signal(false); + showSuccess = signal(false); + showError = signal(false); + errorMessage = signal(''); + successMessage = signal(''); + generatedPassword = signal(''); + + // No need for icons object, icons are available directly + cilLockLocked = cilLockLocked; + cilUser = cilUser; + cilCheckCircle = cilCheckCircle; + cilXCircle = cilXCircle; + + constructor() { + this.changePasswordForm = this.fb.group({ + username: ['', [Validators.required, Validators.minLength(3)]] + }); + } + + ngOnInit(): void { + // Check if user is admin + const currentUser = this.authService.currentUser(); + if (!currentUser?.isAdmin) + {this.router.navigate(['/dashboard']);} + + } + + generateRandomPassword(): string { + const length = 8; + const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let password = ''; + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * charset.length); + password += charset[randomIndex]; + } + return password; + } + + async onSubmit(): Promise { + if (this.changePasswordForm.invalid) { + Object.keys(this.changePasswordForm.controls).forEach(key => { + this.changePasswordForm.get(key)?.markAsTouched(); + }); + return; + } + + this.isLoading.set(true); + this.showSuccess.set(false); + this.showError.set(false); + this.errorMessage.set(''); + this.successMessage.set(''); + this.generatedPassword.set(''); + + try { + const token = this.authService.getToken(); + if (!token) { + this.showError.set(true); + this.errorMessage.set('Sessione scaduta. Effettua nuovamente il login.'); + this.isLoading.set(false); + return; + } + + const { username } = this.changePasswordForm.value; + const newPassword = this.generateRandomPassword(); + this.generatedPassword.set(newPassword); + + const result = await firstValueFrom( + this.apiService.post('/auth/admin-change-password', { + userName: username, + newPassword: newPassword, + jwtToken: token + }) + ); + + this.isLoading.set(false); + + if (result.success) { + this.showSuccess.set(true); + this.successMessage.set(`Password modificata con successo per l'utente ${username}. L'utente dovrà effettuare nuovamente il login.`); + this.changePasswordForm.reset(); + } else { + this.showError.set(true); + this.errorMessage.set('Impossibile modificare la password. Verifica che l\'utente esista e riprova.'); + this.generatedPassword.set(''); + } + } catch (error) { + console.error('Error changing password:', error); + this.isLoading.set(false); + this.showError.set(true); + this.errorMessage.set('Si è verificato un errore durante la modifica della password. Riprova.'); + this.generatedPassword.set(''); + } + } + + getErrorMessage(fieldName: string): string { + const control = this.changePasswordForm.get(fieldName); + + if (!control || !control.touched || !control.errors) + {return '';} + + + if (control.errors['required']) + {return 'Questo campo è obbligatorio';} + + + if (fieldName === 'username') + {if (control.errors['minlength']) + {return 'Il nome utente deve contenere almeno 3 caratteri';}} + + + + return ''; + } + + isFieldInvalid(fieldName: string): boolean { + const control = this.changePasswordForm.get(fieldName); + return !!(control && control.invalid && control.touched); + } + + copyToClipboard(text: string): void { + navigator.clipboard.writeText(text).catch(err => { + console.error('Errore durante la copia:', err); + }); + } +} diff --git a/client/src/app/views/admin-change-password/routes.ts b/client/src/app/views/admin-change-password/routes.ts new file mode 100644 index 000000000..4cf0bde8b --- /dev/null +++ b/client/src/app/views/admin-change-password/routes.ts @@ -0,0 +1,11 @@ +import { Routes } from '@angular/router'; + +export const routes: Routes = [ + { + path: '', + loadComponent: () => import('./admin-change-password.component').then(m => m.AdminChangePasswordComponent), + data: { + title: 'Modifica Password Utente' + } + } +]; diff --git a/client/src/app/views/admin/admin.component.html b/client/src/app/views/admin/admin.component.html new file mode 100644 index 000000000..67156241b --- /dev/null +++ b/client/src/app/views/admin/admin.component.html @@ -0,0 +1,340 @@ +
+ + +

Risultati Gare

+

+ In questa sezione puoi inserire i risultati delle gare del campionato +

+
+ + +
+ + + @if (isSeasonLoading()) { +
+ + Caricamento stagione... +
+ } +
+
+
+ + + @if (isInitialLoading()) { + + +
+ +

Caricamento dati amministrazione...

+

Attendere il caricamento dei dati

+
+
+
+ } @else { + + + + @for (track of tracks(); track track.track_id) { + + {{ track.name }} + + + + +
+ +
+ + + + + + + + + + @if (track.has_sprint == 1) { + + } + + + + + + @for (posizione of posizioni() | keyvalue; track posizione.key + '_' + $index) { + + + + + + + + + @if (track.has_sprint == 1) { + + } + + + + + + + + } + +
#GaraSprintQualificaProve Libere
+ + @if (posizione.key <= 8) { + {{ posizione?.value }} posto + } @else { + {{posizione?.value}} + } + + @if (posizione.key <= 3) { + + } + @if (posizione.key == 9) { + + } + @if (posizione.key == 10) { + + } + + @if (posizione.key < 10) { + + } + @if (posizione.key == 10) { +
+ + +
+ } +
+ @if (posizione.key < 10) { + + } + @if (posizione.key == 10) { +
+ + +
+ } +
+ @if (posizione.key < 9) { + + } + + @if (posizione.key < 9) { + + } +
+
+
+ + + + + + + + @if (formStatus()[track.track_id] == 2) { +
+ Errore nella validazione dei risultati + @if (formErrors()[track.track_id] && formErrors()[track.track_id].length > 0) { +
+ @for (error of formErrors()[track.track_id]; track $index) { + {{ error }} + } +
+ } +
+ } + @if (formStatus()[track.track_id] == 3) { +
+ Errore nel salvataggio + @if (formErrors()[track.track_id] && formErrors()[track.track_id].length > 0) { +
+ @for (error of formErrors()[track.track_id]; track $index) { + {{ error }} + } +
+ } +
+ } + @if (formStatus()[track.track_id] == 1) { +

+ Risultati salvati correttamente :) +

+ } +
+
+
+
+
+
+
+ } +
+
+ } +
+
\ No newline at end of file diff --git a/client/src/app/views/admin/admin.component.scss b/client/src/app/views/admin/admin.component.scss new file mode 100644 index 000000000..e0e4812fa --- /dev/null +++ b/client/src/app/views/admin/admin.component.scss @@ -0,0 +1,311 @@ +/* Apply to the dropdown via your panelClass */ +.multi-select-option, +.multi-select-option.mat-mdc-option-active, +.multi-select-option.mdc-list-item--activated { + background-color: white !important; + color: black !important; /* black text */ +} + +.multi-select-option:hover +{ + background-color: #E9E9ED !important; +} + +// Enhanced Admin Form Styling + +// Season selector styling to match CoreUI theme +/* Moved to global _custom-select.scss - class .global-select-group */ + +// Enhanced styling for all admin select elements +.admin-select { + padding: 0.4rem 0.6rem !important; + border: 1px solid var(--cui-border-color) !important; + border-radius: 6px !important; + background: var(--cui-body-bg) !important; + color: var(--cui-body-color) !important; + font-size: 0.8rem !important; + transition: all 0.3s ease !important; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; + min-height: 32px !important; + line-height: 1.2 !important; + + &:focus { + border-color: var(--cui-primary) !important; + box-shadow: 0 0 0 0.1rem rgba(var(--cui-primary-rgb), 0.2) !important; + outline: none !important; + } + + &:hover:not(:focus) { + border-color: rgba(var(--cui-primary-rgb), 0.4) !important; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; + } + + &:invalid { + border-color: var(--cui-danger) !important; + box-shadow: 0 0 0 0.1rem rgba(var(--cui-danger-rgb), 0.15) !important; + } + + // Multi-select specific styling + &.multi-select { + min-height: 70px !important; + padding: 0.4rem !important; + + option { + padding: 0.3rem 0.5rem; + margin: 0.05rem 0; + border-radius: 3px; + transition: all 0.2s ease; + font-size: 0.8rem; + line-height: 1.3; + + &:checked { + background: linear-gradient(135deg, var(--cui-primary), var(--cui-info)) !important; + color: white !important; + font-weight: 500; + } + + &:hover { + background: rgba(var(--cui-primary-rgb), 0.1) !important; + color: var(--cui-primary) !important; + } + } + } + + option { + padding: 0.4rem 0.6rem; + background: var(--cui-body-bg); + color: var(--cui-body-color); + border: none; + font-size: 0.8rem; + line-height: 1.3; + transition: all 0.2s ease; + + &:hover { + background: linear-gradient(135deg, rgba(var(--cui-primary-rgb), 0.1), rgba(var(--cui-info-rgb), 0.05)) !important; + color: var(--cui-primary) !important; + } + + &:checked, + &:selected { + background: linear-gradient(135deg, var(--cui-primary), var(--cui-info)) !important; + color: white !important; + font-weight: 500; + } + + // Style for the default option + &[value="0"] { + color: var(--cui-text-muted) !important; + font-style: italic; + background: var(--cui-tertiary-bg) !important; + } + } +} + +// Multi-select container styling +.multi-select-container { + .multi-select-label { + display: block; + margin-bottom: 0.3rem; + font-size: 0.75rem; + font-weight: 500; + color: var(--cui-text-muted); + } +} + +// Table enhancements - Reduced height +table[cTable] { + .text-center { + vertical-align: middle; + } + + td { + padding: 0.4rem 0.3rem !important; + vertical-align: middle; + + .admin-select { + margin: 0; + width: 100%; + min-width: 140px; + max-width: 160px; + } + + .multi-select-container { + width: 100%; + min-width: 150px; + max-width: 170px; + } + + // Special styling for position column + &:first-child { + padding: 0.6rem 0.4rem !important; + min-width: 120px; + + strong { + font-size: 0.85rem; + font-weight: 600; + } + + img, svg { + margin-left: 0.25rem; + } + } + } + + th { + font-weight: 600; + font-size: 0.85rem; + padding: 0.75rem 0.3rem !important; + text-align: center; + + &:first-child { + text-align: left; + padding-left: 0.6rem !important; + } + } + + // Striped rows with better contrast + tbody tr:nth-child(odd) { + background-color: rgba(var(--cui-primary-rgb), 0.02); + } + + tbody tr:hover { + background-color: rgba(var(--cui-primary-rgb), 0.05); + } +} + +// Button enhancements +button[cButton] { + padding: 0.75rem 2rem; + font-weight: 600; + border-radius: 8px; + transition: all 0.3s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + + &:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + } + + &:active { + transform: translateY(0); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + } +} + +// Submit button with loading state +.submit-button { + min-width: 120px; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +// Loading animations +.initial-loading { + padding: 3rem 2rem; + text-align: center; + + .spinner-border { + width: 3rem; + height: 3rem; + display: inline-block; + margin-bottom: 1rem; + } + + h4 { + color: var(--cui-body-color); + font-weight: 600; + margin-bottom: 0.5rem; + } + + p { + color: var(--cui-text-muted); + margin-bottom: 0; + } +} + +// Badge styling improvements +c-badge { + font-weight: 500; + padding: 0.5rem 0.75rem; + border-radius: 6px; +} + +// Accordion improvements +c-accordion-item { + border-radius: 8px; + margin-bottom: 1rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + overflow: hidden; + + .accordion-body { + padding: 2rem; + } +} + +// Responsive improvements +@media (max-width: 768px) { + .admin-select { + font-size: 0.75rem !important; + padding: 0.35rem 0.5rem !important; + min-height: 30px !important; + } + + .season-selector-group { + margin-bottom: 1rem; + + .season-select { + padding: 0.6rem 0.8rem; + font-size: 0.85rem; + } + } + + table[cTable] { + td { + padding: 0.35rem 0.2rem !important; + + .admin-select { + min-width: 110px; + max-width: 130px; + font-size: 0.75rem; + } + + .multi-select-container { + min-width: 120px; + max-width: 140px; + } + + &:first-child { + padding: 0.5rem 0.3rem !important; + min-width: 100px; + + strong { + font-size: 0.8rem; + } + } + } + + th { + padding: 0.6rem 0.2rem !important; + font-size: 0.8rem; + + &:first-child { + padding-left: 0.4rem !important; + } + } + } + + // Compact accordion on mobile + c-accordion-item { + .accordion-body { + padding: 1rem; + } + } +} \ No newline at end of file diff --git a/client/src/app/views/admin/admin.component.spec.ts b/client/src/app/views/admin/admin.component.spec.ts new file mode 100644 index 000000000..887045ba2 --- /dev/null +++ b/client/src/app/views/admin/admin.component.spec.ts @@ -0,0 +1,225 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { signal } from '@angular/core'; +import { Router } from '@angular/router'; + +import { AdminComponent } from './admin.component'; +import { DbDataService } from 'src/app/service/db-data.service'; +import { AuthService } from 'src/app/service/auth.service'; +import type { Season, Driver, TrackData, ChampionshipData } from '@f123dashboard/shared'; + +describe('AdminComponent', () => { + let component: AdminComponent; + let fixture: ComponentFixture; + let mockDbDataService: jasmine.SpyObj; + let mockAuthService: jasmine.SpyObj; + let mockRouter: jasmine.SpyObj; + + const mockSeasons: Season[] = [ + { id: 1, description: 'Season 2024', startDate: new Date('2024-01-01'), endDate: undefined } + ]; + + const mockDrivers: Driver[] = [ + { id: 1, username: 'driver1', first_name: 'John', surname: 'Doe' } + ]; + + const mockTracks: TrackData[] = [ + { track_id: 1, name: 'Monaco', country: 'Monaco', date: '2024-05-26T14:00:00Z', has_sprint: 0, has_x2: 0, besttime_driver_time: '1:10.123', username: 'Driver1' } + ]; + + const mockChampionship: ChampionshipData[] = []; + + beforeEach(async () => { + mockDbDataService = jasmine.createSpyObj('DbDataService', [ + 'getAllSeasons', + 'getDriversData', + 'getAllTracksBySeason', + 'getChampionshipBySeason', + 'setGpResult' + ]); + + mockAuthService = jasmine.createSpyObj('AuthService', [], { currentUser: signal({ isAdmin: true, id: 1, username: 'admin', name: 'Admin', surname: 'User' }) }); + mockRouter = jasmine.createSpyObj('Router', ['navigate']); + + mockDbDataService.getAllSeasons.and.returnValue(Promise.resolve(mockSeasons)); + mockDbDataService.getDriversData.and.returnValue(Promise.resolve(mockDrivers)); + mockDbDataService.getAllTracksBySeason.and.returnValue(Promise.resolve(mockTracks)); + mockDbDataService.getChampionshipBySeason.and.returnValue(Promise.resolve(mockChampionship)); + + await TestBed.configureTestingModule({ + providers: [ + provideNoopAnimations(), + { provide: DbDataService, useValue: mockDbDataService }, + { provide: AuthService, useValue: mockAuthService }, + { provide: Router, useValue: mockRouter } + ], + imports: [AdminComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AdminComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should redirect non-admin users to dashboard', async () => { + TestBed.resetTestingModule(); + const nonAdminAuthService = jasmine.createSpyObj('AuthService', [], { currentUser: signal({ isAdmin: false, id: 2, username: 'user', name: 'Regular', surname: 'User' }) }); + const testRouter = jasmine.createSpyObj('Router', ['navigate']); + const testDbDataService = jasmine.createSpyObj('DbDataService', [ + 'getAllSeasons', + 'getDriversData', + 'getAllTracksBySeason', + 'getChampionshipBySeason', + 'setGpResult' + ]); + + await TestBed.configureTestingModule({ + providers: [ + provideNoopAnimations(), + { provide: DbDataService, useValue: testDbDataService }, + { provide: AuthService, useValue: nonAdminAuthService }, + { provide: Router, useValue: testRouter } + ], + imports: [AdminComponent] + }).compileComponents(); + + const testFixture = TestBed.createComponent(AdminComponent); + const testComponent = testFixture.componentInstance; + + await testComponent.ngOnInit(); + + expect(testRouter.navigate).toHaveBeenCalledWith(['/dashboard']); + }); + + it('should load seasons on initialization', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.seasons()).toEqual(mockSeasons); + expect(component.selectedSeason()).toBe(1); + }); + + it('should set loading states correctly during initialization', async () => { + expect(component.isInitialLoading()).toBe(true); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.isInitialLoading()).toBe(false); + }); + + it('should load season data when season changes', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + + component.selectedSeason.set(1); + await component.onSeasonChange(); + + expect(mockDbDataService.getDriversData).toHaveBeenCalledWith(1); + expect(mockDbDataService.getAllTracksBySeason).toHaveBeenCalledWith(1); + expect(mockDbDataService.getChampionshipBySeason).toHaveBeenCalledWith(1); + }); + + it('should validate race results with DNF correctly', () => { + const validResults = [1, 0, 0, 0, 0, 0, 0, 0, 1, [1]]; // One driver finished, one DNF + const result = component.validateSessionWithDnf(validResults, 'Test'); + + expect(result.isValid).toBe(false); // Will fail because piloti signal is empty in test + }); + + it('should validate qualifying results without DNF correctly', () => { + component.piloti.set(mockDrivers); + const validResults = [1, 0, 0, 0, 0, 0, 0, 0]; + const result = component.validateSessionNoDnf(validResults, 'Qualifying'); + + expect(result.isValid).toBe(false); // Missing drivers + expect(result.errors.length).toBeGreaterThan(0); + }); + + it('should get and set race results correctly', () => { + const trackId = 1; + const position = 1; + const driverId = 5; + + component.setRaceResult(trackId, position, driverId); + const result = component.getRaceResult(trackId, position); + + expect(result).toBe(driverId); + }); + + it('should get and set sprint results correctly', () => { + const trackId = 1; + const position = 2; + const driverId = 3; + + component.setSprintResult(trackId, position, driverId); + const result = component.getSprintResult(trackId, position); + + expect(result).toBe(driverId); + }); + + it('should get and set qualifying results correctly', () => { + const trackId = 1; + const position = 3; + const driverId = 7; + + component.setQualiResult(trackId, position, driverId); + const result = component.getQualiResult(trackId, position); + + expect(result).toBe(driverId); + }); + + it('should get and set free practice results correctly', () => { + const trackId = 1; + const position = 1; + const driverId = 2; + + component.setFpResult(trackId, position, driverId); + const result = component.getFpResult(trackId, position); + + expect(result).toBe(driverId); + }); + + it('should detect duplicate drivers in positions', () => { + component.piloti.set([ + { id: 1, username: 'd1', first_name: 'A', surname: 'B' }, + { id: 2, username: 'd2', first_name: 'C', surname: 'D' } + ]); + + const positions = [1, 1]; // Duplicate driver + const errors = component.getDriverValidationErrors(positions); + + expect(errors.length).toBeGreaterThan(0); + expect(errors.some(e => e.includes('più volte'))).toBe(true); + }); + + it('should return empty array for race results of non-existent track', () => { + const result = component.getRaceResult(999, 1); + expect(result).toBe(0); + }); + + it('should handle empty DNF array in race results', () => { + const trackId = 1; + component.setRaceResult(trackId, 10, []); + const result = component.getRaceResult(trackId, 10); + + expect(Array.isArray(result)).toBe(true); + }); + + it('should initialize results maps correctly', () => { + component.piloti.set(mockDrivers); + component.tracks.set(mockTracks); + component.championshipData.set(mockChampionship); + + component.initializeResults(); + + expect(component.raceResults()).toBeInstanceOf(Map); + expect(component.sprintResults()).toBeInstanceOf(Map); + expect(component.qualiResults()).toBeInstanceOf(Map); + expect(component.fpResults()).toBeInstanceOf(Map); + }); +}); diff --git a/client/src/app/views/admin/admin.component.ts b/client/src/app/views/admin/admin.component.ts new file mode 100644 index 000000000..15edc1f8c --- /dev/null +++ b/client/src/app/views/admin/admin.component.ts @@ -0,0 +1,588 @@ +import { Component, OnInit, inject, ChangeDetectionStrategy, signal, computed } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, NgForm, ReactiveFormsModule, FormControl } from '@angular/forms'; +import { MatSelectModule } from '@angular/material/select'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { Router } from '@angular/router'; +import { + AccordionComponent, + AccordionItemComponent, + AccordionButtonDirective, + ContainerComponent, + RowComponent, + ColComponent, + TemplateIdDirective, + TableDirective, + BadgeComponent, + GridModule, + ButtonDirective +} from '@coreui/angular'; +import { cilFire, cilPowerStandby } from '@coreui/icons'; +import { IconDirective } from '@coreui/icons-angular'; + +import { DbDataService } from 'src/app/service/db-data.service'; +import { AuthService } from 'src/app/service/auth.service'; +import { GpResult } from '../../model/championship'; +import { medals, allFlags, posizioni } from '../../model/constants'; +import type { ChampionshipData, Driver, Season, TrackData } from '@f123dashboard/shared'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-admin', + imports: [ + AccordionComponent, + AccordionItemComponent, + RowComponent, + ColComponent, + ContainerComponent, + CommonModule, + IconDirective, + AccordionButtonDirective, + TemplateIdDirective, + TableDirective, + BadgeComponent, + FormsModule, + ReactiveFormsModule, + GridModule, + ButtonDirective, + MatFormFieldModule, + MatSelectModule + ], + templateUrl: './admin.component.html', + styleUrl: './admin.component.scss' +}) + +export class AdminComponent implements OnInit { + private dbData = inject(DbDataService); + private authService = inject(AuthService); + private router = inject(Router); + + + // VARIABLE DEFINITIONS + tracks = signal([]); + piloti = signal([]); + championshipData = signal([]); + seasons = signal([]); + selectedSeason = signal(null); + formStatus = signal>({}); // 0: none, 1: success, 2: validation error, 3: backend error + formErrors = signal>({}); + raceResults = signal>(new Map()); // [track_id, array_of_results] + sprintResults = signal>(new Map()); + qualiResults = signal>(new Map()); + fpResults = signal>(new Map()); + + // Loading states + isInitialLoading = signal(true); + isSeasonLoading = signal(false); + isSubmitting = signal>({}); + + public allFlags = allFlags; + public medals = medals; + public posizioni = computed(() => new Map([...posizioni].filter(([key]) => key !== 11))); + public fireIcon: string[] = cilFire; + public powerIcon: string[] = cilPowerStandby; + + toppings = new FormControl(''); + + // FUNCTION DEFINTIONS + async ngOnInit(): Promise { + this.isInitialLoading.set(true); + try { + // Additional security check + const currentUser = this.authService.currentUser(); + if (!currentUser?.isAdmin) { + this.router.navigate(['/dashboard']); + return; + } + + // Load seasons first + const seasons = await this.dbData.getAllSeasons(); + this.seasons.set(seasons); + + // Get the latest season (first in the list since it's ordered by start_date DESC) + if (seasons.length > 0) { + this.selectedSeason.set(seasons[0].id); + } + + // Load data for the selected season + await this.loadSeasonData(); + } catch (error) { + console.error('Errore durante il caricamento dei dati della stagione:', error); + this.isInitialLoading.set(false); + } finally { + this.isInitialLoading.set(false); + } + } + + async loadSeasonData(): Promise { + this.isSeasonLoading.set(true); + try { + const seasonId = this.selectedSeason(); + let piloti: Driver[]; + let tracks: TrackData[]; + let championshipData: ChampionshipData[]; + + if (seasonId) { + // Load data for specific season concurrently + [piloti, tracks, championshipData] = await Promise.all([ + this.dbData.getDriversData(seasonId), + this.dbData.getAllTracksBySeason(seasonId), + this.dbData.getChampionshipBySeason(seasonId) + ]); + } else { + // Load data for latest season (default) concurrently + [piloti, tracks, championshipData] = await Promise.all([ + this.dbData.getDriversData(), + this.dbData.getAllTracksBySeason(), + this.dbData.getChampionshipBySeason() + ]); + } + + this.piloti.set(piloti); + this.tracks.set(tracks); + this.championshipData.set(championshipData); + + this.initializeResults(); + } catch (error) { + console.error('Errore durante il caricamento dei dati della stagione:', error); + this.isSeasonLoading.set(false); + } finally { + this.isSeasonLoading.set(false); + } + } + + async onSeasonChange(): Promise { + await this.loadSeasonData(); + } + + initializeResults() { + const pilotiMap: Map = new Map(); // map to quickly search driver_id given its driver_username + for (const pilota of this.piloti()) + {pilotiMap.set(pilota.username, pilota.id);} + + const raceResultsMap = new Map(); + const sprintResultsMap = new Map(); + const qualiResultsMap = new Map(); + const fpResultsMap = new Map(); + + for (const gp of this.championshipData()) { + const track = this.tracks().find(t => t.name == gp.track_name); + if (!track) {continue;} // Skip if track not found + + const trackId = track.track_id; + + // Initialize race results + let race: any[] = []; + const activeRaceSession = gp.gran_prix_has_x2 === 1 ? gp.sessions.full_race : gp.sessions.race; + const activeFastLapDriver = gp.gran_prix_has_x2 === 1 ? gp.fastLapDrivers.full_race : gp.fastLapDrivers.race; + + if (activeRaceSession && activeRaceSession.length > 0) { + // Fill positions 1-8 + for (let i = 1; i <= 8; i++) { + const driver = activeRaceSession.find(r => r.position === i); + race[i-1] = driver ? pilotiMap.get(driver.driver_username) || 0 : 0; + } + + // Fast lap driver (position 9 in array, index 8) + race[8] = activeFastLapDriver ? pilotiMap.get(activeFastLapDriver) || 0 : 0; + + // DNF drivers (position 10 in array, index 9) + const dnfDrivers = activeRaceSession.filter(r => r.position === 0); + race[9] = dnfDrivers.map(d => pilotiMap.get(d.driver_username)).filter(id => id !== undefined); + } else + // Initialize empty array if no results + {race = [0, 0, 0, 0, 0, 0, 0, 0, []];} + + + // Initialize sprint results + const sprint: any[] = []; + if (gp.gran_prix_has_sprint === 1 && gp.sessions.sprint) { + // Fill positions 1-8 + for (let i = 1; i <= 8; i++) { + const driver = gp.sessions.sprint.find(r => r.position === i); + sprint[i-1] = driver ? pilotiMap.get(driver.driver_username) || 0 : 0; + } + + // Fast lap driver (position 9 in array, index 8) + sprint[8] = gp.fastLapDrivers.sprint ? pilotiMap.get(gp.fastLapDrivers.sprint) || 0 : 0; + + // DNF drivers (position 10 in array, index 9) + const dnfDrivers = gp.sessions.sprint.filter(r => r.position === 0); + sprint[9] = dnfDrivers.map(d => pilotiMap.get(d.driver_username)).filter(id => id !== undefined); + } + + // Initialize qualifying results + let quali: any[] = []; + if (gp.sessions.qualifying) + {for (let i = 1; i <= 8; i++) { + const driver = gp.sessions.qualifying.find(r => r.position === i); + quali[i-1] = driver ? pilotiMap.get(driver.driver_username) || 0 : 0; + }} + else + {quali = [0, 0, 0, 0, 0, 0, 0, 0];} + + + // Initialize free practice results + let fp: any[] = []; + if (gp.sessions.free_practice) + {for (let i = 1; i <= 8; i++) { + const driver = gp.sessions.free_practice.find(r => r.position === i); + fp[i-1] = driver ? pilotiMap.get(driver.driver_username) || 0 : 0; + }} + else + {fp = [0, 0, 0, 0, 0, 0, 0, 0];} + + + raceResultsMap.set(trackId, race); + sprintResultsMap.set(trackId, sprint); + qualiResultsMap.set(trackId, quali); + fpResultsMap.set(trackId, fp); + } + + this.raceResults.set(raceResultsMap); + this.sprintResults.set(sprintResultsMap); + this.qualiResults.set(qualiResultsMap); + this.fpResults.set(fpResultsMap); + } + + + async publishResult(trackId: number, hasSprint: string, form: NgForm): Promise { + const seasonId = this.selectedSeason(); + if (!seasonId) + {throw new Error('Nessuna stagione selezionata');} + + console.log('Publishing results for trackId:', trackId, 'seasonId:', seasonId); + this.isSubmitting.update(submitting => ({ ...submitting, [trackId]: true })); + + // Clear previous errors and status + this.formErrors.update(errors => ({ ...errors, [trackId]: [] })); + this.formStatus.update(status => ({ ...status, [trackId]: 0 })); // Reset status + + try { + // check data validity + const hasSprintBool = hasSprint === "1"; + if ( this.formIsValid(trackId, hasSprintBool) ) { + const raceData = this.raceResults().get(trackId)!; + const raceDnfResultTmp: number[] = raceData[9]; + let sprintDnfResultTmp: number[] = []; + if ( hasSprintBool ) { + const sprintData = this.sprintResults().get(trackId)!; + sprintDnfResultTmp = sprintData[9]; + } + + + const gpResult: GpResult = { + trackId: trackId, + hasSprint: hasSprintBool, + raceResult: Array.from(raceData.values()).slice(0, 9).map(x => Number(x)), + raceDnfResult: raceDnfResultTmp ? raceDnfResultTmp.map(x => Number(x)) : [], + sprintResult: hasSprintBool ? Array.from(this.sprintResults().get(trackId)!.values()).slice(0, 9).map(x => Number(x)) : [], + sprintDnfResult: sprintDnfResultTmp ? sprintDnfResultTmp.map(x => Number(x)) : [], + qualiResult: Array.from(this.qualiResults().get(trackId)!.values()).map(x => Number(x)), + fpResult: Array.from(this.fpResults().get(trackId)!.values()).map(x => Number(x)), + seasonId: +seasonId + } + + try { + const result = await this.dbData.setGpResult(trackId, gpResult); + + // Check if the result indicates success + if (result && typeof result === 'string') { + const parsedResult = JSON.parse(result); + if (parsedResult.success) { + this.formStatus.update(status => ({ ...status, [trackId]: 1 })); // Success + this.formErrors.update(errors => ({ ...errors, [trackId]: [] })); // Clear any previous errors + console.log('Risultati salvati con successo:', parsedResult.message); + } else { + this.formStatus.update(status => ({ ...status, [trackId]: 3 })); // Backend error + this.formErrors.update(errors => ({ ...errors, [trackId]: [`Errore del server: ${parsedResult.message || 'Errore sconosciuto'}`] })); + console.error('Errore dal backend:', parsedResult.message); + } + } else { + // Assume success if no specific response format + this.formStatus.update(status => ({ ...status, [trackId]: 1 })); // Success + this.formErrors.update(errors => ({ ...errors, [trackId]: [] })); // Clear any previous errors + console.log('Risultati salvati con successo'); + } + } catch (backendError) { + this.formStatus.update(status => ({ ...status, [trackId]: 3 })); // Backend error + this.formErrors.update(errors => ({ ...errors, [trackId]: [`Errore di comunicazione con il server: ${backendError instanceof Error ? backendError.message : 'Errore sconosciuto'}`] })); + console.error('Errore nella chiamata al backend:', backendError); + } + } + else + {this.formStatus.update(status => ({ ...status, [trackId]: 2 }));} // Validation errors + + } catch (error) { + this.formStatus.update(status => ({ ...status, [trackId]: 3 })); // General error + this.formErrors.update(errors => ({ ...errors, [trackId]: [`Errore generale: ${error instanceof Error ? error.message : 'Errore sconosciuto'}`] })); + console.error('Errore generale in publishResult:', error); + } finally { + this.isSubmitting.update(submitting => ({ ...submitting, [trackId]: false })); + } + } + + formIsValid(trackId: number, hasSprint: boolean): boolean { + let isValid = true; + const errorMessages: string[] = []; + + // Validate race results (with DNF support) + const raceValid = this.validateSessionWithDnf(this.raceResults().get(trackId) || [], 'Gara'); + isValid = isValid && raceValid.isValid; + if (!raceValid.isValid) + {raceValid.errors.forEach(error => errorMessages.push(`Gara: ${error}`));} + + + // Validate qualifying results (no DNF) + const qualiValid = this.validateSessionNoDnf(this.qualiResults().get(trackId) || [], 'Qualifica'); + isValid = isValid && qualiValid.isValid; + if (!qualiValid.isValid) + {qualiValid.errors.forEach(error => errorMessages.push(`Qualifica: ${error}`));} + + + // Validate free practice results (no DNF) + const fpValid = this.validateSessionNoDnf(this.fpResults().get(trackId) || [], 'Prove Libere'); + isValid = isValid && fpValid.isValid; + if (!fpValid.isValid) + {fpValid.errors.forEach(error => errorMessages.push(`Prove Libere: ${error}`));} + + + // Validate sprint results if applicable (with DNF support) + if (hasSprint) { + const sprintValid = this.validateSessionWithDnf(this.sprintResults().get(trackId) || [], 'Sprint'); + isValid = isValid && sprintValid.isValid; + if (!sprintValid.isValid) + {sprintValid.errors.forEach(error => errorMessages.push(`Sprint: ${error}`));} + + } + + // Store error messages for this track + this.formErrors.update(errors => ({ ...errors, [trackId]: errorMessages })); + + // Log errors for debugging + if (!isValid) + {console.error('Errori di validazione form:', errorMessages);} + + + return isValid; + } + + hasAllPlayersExactlyOnce(positions: number[]): boolean { + return this.getDriverValidationErrors(positions).length === 0; + } + + getDriverValidationErrors(positions: number[]): string[] { + const errors: string[] = []; + const piloti = this.piloti(); + const driverCount = piloti.length; + const validPositions = positions.filter(p => Number(p) > 0).map(p => Number(p)); + const uniqueDrivers = new Set(validPositions); + + // Get actual driver IDs from piloti array + const validDriverIds = new Set(piloti.map(p => +p.id)); + console.log('Valid driver IDs:', Array.from(validDriverIds)); + console.log('typeof validDriverIds:', typeof Array.from(validDriverIds)[0]); + + // Check for duplicate drivers + if (validPositions.length !== uniqueDrivers.size) + {errors.push(`Alcuni piloti sono assegnati più volte`);} + + + // Check for missing or extra drivers + if (uniqueDrivers.size < driverCount) { + const missingCount = driverCount - uniqueDrivers.size; + errors.push(`${missingCount} pilota/i mancante/i dai risultati`); + } else if (uniqueDrivers.size > driverCount) + {errors.push(`Troppi piloti assegnati`);} + + + // Check if all provided driver IDs are valid (exist in piloti array) + for (const driverId of uniqueDrivers) { + console.log('typeof driverId:', typeof driverId); + console.log('driverId:', driverId); + if (!validDriverIds.has(driverId)) { + // Find the driver username if it exists, otherwise show the ID + const driver = piloti.find(p => p.id == driverId); + const driverName = driver ? driver.username : `ID ${driverId}`; + errors.push(`Pilota ${driverName} non esiste`); + break; // Only show first invalid driver ID to avoid spam + } + } + + return errors; + } + + validateSessionWithDnf(resultArray: any[], sessionName: string): { isValid: boolean; errors: string[] } { + const errors: string[] = []; + + if (!resultArray || resultArray.length < 9) { + errors.push(`Dati ${sessionName} incompleti`); + return { isValid: false, errors }; + } + + const positions = resultArray.slice(0, 8); + const fastLap = resultArray[8]; + const dnf: number[] = resultArray.length > 9 && resultArray[9] ? resultArray[9].map((x: any) => Number(x)) : []; + + // Check for duplicates in positions (excluding zeros) + const nonZeroPositions = positions.filter(p => p !== 0); + const positionSet = new Set(nonZeroPositions); + if (nonZeroPositions.length !== positionSet.size) + {errors.push(`Piloti duplicati trovati nelle posizioni`);} + + + // Check fast lap is set + if (!fastLap || fastLap === 0) + {errors.push(`Il pilota del giro veloce deve essere selezionato`);} + + + // Check DNF drivers are not in positions + if (dnf.some(d => positions.includes(d))) + {errors.push(`I piloti DNF non possono essere anche nelle posizioni di gara`);} + + + // Check that positions after DNF count are empty + const emptyPositionsNeeded = dnf.length; + const lastPositions = positions.slice(8 - emptyPositionsNeeded); + if (lastPositions.some(p => p !== 0)) + {errors.push(`Le ultime ${emptyPositionsNeeded} posizioni devono essere vuote quando ${dnf.length} piloti sono DNF`);} + + + // Check all drivers are present exactly once + const allDrivers = nonZeroPositions.concat(dnf); + if (!this.hasAllPlayersExactlyOnce(allDrivers)) { + const validationErrors = this.getDriverValidationErrors(allDrivers); + errors.push(...validationErrors); + } + + return { isValid: errors.length === 0, errors }; + } + + validateSessionNoDnf(resultArray: any[], sessionName: string): { isValid: boolean; errors: string[] } { + const errors: string[] = []; + + if (!resultArray || resultArray.length < 8) { + errors.push(`Dati ${sessionName} incompleti`); + return { isValid: false, errors }; + } + + const positions = resultArray.slice(0, 8); + + // Check for duplicates in positions (excluding zeros) + const nonZeroPositions = positions.filter(p => p !== 0); + const positionSet = new Set(nonZeroPositions); + if (nonZeroPositions.length !== positionSet.size) + {errors.push(`Piloti duplicati trovati nelle posizioni`);} + + + // Check all positions are filled + if (positions.some(p => p === 0 || p === null || p === undefined)) + {errors.push(`Tutte le posizioni devono essere compilate`);} + + + // Check all drivers are present exactly once + if (!this.hasAllPlayersExactlyOnce(positions)) { + const validationErrors = this.getDriverValidationErrors(positions); + errors.push(...validationErrors); + } + + return { isValid: errors.length === 0, errors }; + } + + getRaceResult(trackId: number, position: number): any { + const resultArray = this.raceResults().get(trackId) || []; + return resultArray[position - 1] || 0; + } + + getSprintResult(trackId: number, position: number): any { + const resultArray = this.sprintResults().get(trackId) || []; + return resultArray[position - 1] || 0; + } + + getQualiResult(trackId: number, position: number): any { + const resultArray = this.qualiResults().get(trackId) || []; + return resultArray[position - 1] || 0; + } + + getFpResult(trackId: number, position: number): any { + const resultArray = this.fpResults().get(trackId) || []; + return resultArray[position - 1] || 0; + } + + setRaceResult(trackId: number, position: number, valore: any): void { + if(valore) { + this.raceResults.update(results => { + const newResults = new Map(results); + let resultArray = newResults.get(trackId); + if (!resultArray) { + resultArray = []; + newResults.set(trackId, resultArray); + } + if ( position - 1 < 9) + {resultArray[position-1] = +valore;} + else { + if ( !resultArray[position-1] ) + + {resultArray[position-1] = []} + + resultArray[position-1] = valore; + } + return newResults; + }); + } + } + + setSprintResult(trackId: number, position: number, valore: any): void { + if(valore) { + this.sprintResults.update(results => { + const newResults = new Map(results); + let resultArray = newResults.get(trackId); + if (!resultArray) { + resultArray = []; + newResults.set(trackId, resultArray); + } + if ( position - 1 < 9) + {resultArray[position-1] = +valore;} + else { + if ( !resultArray[position-1] ) + + {resultArray[position-1] = []} + + resultArray[position-1] = valore; + } + return newResults; + }); + } + } + + setQualiResult(trackId: number, position: number, valore: any): void { + if(valore) { + this.qualiResults.update(results => { + const newResults = new Map(results); + let resultArray = newResults.get(trackId); + if (!resultArray) { + resultArray = []; + newResults.set(trackId, resultArray); + } + resultArray[position-1] = +valore; + return newResults; + }); + } + } + + setFpResult(trackId: number, position: number, valore: any): void { + if(valore) { + this.fpResults.update(results => { + const newResults = new Map(results); + let resultArray = newResults.get(trackId); + if (!resultArray) { + resultArray = []; + newResults.set(trackId, resultArray); + } + resultArray[position-1] = +valore; + return newResults; + }); + } + } + +} diff --git a/client/src/app/views/admin/gp-edit/gp-edit.component.html b/client/src/app/views/admin/gp-edit/gp-edit.component.html new file mode 100644 index 000000000..78b2582c4 --- /dev/null +++ b/client/src/app/views/admin/gp-edit/gp-edit.component.html @@ -0,0 +1,169 @@ + + +

Gestione GranPremi

+
+ + + + + Azioni di Massa + + +
+ + + + + + + + + +
+
+
+
+
+ + + + + + Aggiungi Nuovo Gran Premio + + +
+ + + + + + + + + +
+ + +
+
+ +
+ + +
+
+ + + +
+
+
+
+
+ + + + + + Prossime Gare + + + @if (loading()) { +
+ +
+ } @else { + + + + + + + + + + + + @for (gp of gps(); track gp.id) { + + + + + + + + } + +
PistaData (UTC)SprintPunti DoppiAzioni
+ + {{ gp.track_name }} + + + +
+ +
+
+
+ +
+
+ + +
+ } +
+
+
+
+ + + @for (toast of toasts(); track $index) { + + + {{ toast.title }} + + + {{ toast.message }} + + + } + + + + +
{{ modalTitle() }}
+ + +
+ + {{ modalMessage() }} + + + + + +
+
diff --git a/client/src/app/views/admin/gp-edit/gp-edit.component.scss b/client/src/app/views/admin/gp-edit/gp-edit.component.scss new file mode 100644 index 000000000..5aff5e5f9 --- /dev/null +++ b/client/src/app/views/admin/gp-edit/gp-edit.component.scss @@ -0,0 +1,17 @@ +:host { + display: block; +} + +.form-check { + min-height: 1.5rem; + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0; +} + +.table-datetime { + padding: 0.4rem 0.5rem; + min-height: 38px; + font-size: 0.85rem; +} diff --git a/client/src/app/views/admin/gp-edit/gp-edit.component.spec.ts b/client/src/app/views/admin/gp-edit/gp-edit.component.spec.ts new file mode 100644 index 000000000..a844e9058 --- /dev/null +++ b/client/src/app/views/admin/gp-edit/gp-edit.component.spec.ts @@ -0,0 +1,332 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { of, throwError, delay } from 'rxjs'; + +import { GpEditComponent } from './gp-edit.component'; +import { GpEditService } from '../../../service/gp-edit.service'; +import type { GPEditItem } from '@f123dashboard/shared'; + +describe('GpEditComponent', () => { + let component: GpEditComponent; + let fixture: ComponentFixture; + let mockGpEditService: jasmine.SpyObj; + + const mockGpData: GPEditItem[] = [ + { + id: 1, + track_id: 1, + track_name: 'Monaco', + date: new Date('2024-05-26T14:00:00Z'), + has_sprint: false, + has_x2: false + } + ]; + + const mockTracks = [ + { id: 1, name: 'Monaco' }, + { id: 2, name: 'Silverstone' } + ]; + + beforeEach(async () => { + mockGpEditService = jasmine.createSpyObj('GpEditService', [ + 'getUpcomingGps', + 'getAllTracks', + 'bulkUpdateGpDate', + 'createGp', + 'updateGp', + 'deleteGp' + ]); + + mockGpEditService.getUpcomingGps.and.returnValue(of({ success: true, data: mockGpData })); + mockGpEditService.getAllTracks.and.returnValue(of({ success: true, data: mockTracks })); + mockGpEditService.bulkUpdateGpDate.and.returnValue(of({ success: true })); + mockGpEditService.createGp.and.returnValue(of({ success: true, data: mockGpData[0] })); + mockGpEditService.updateGp.and.returnValue(of({ success: true })); + mockGpEditService.deleteGp.and.returnValue(of({ success: true })); + + await TestBed.configureTestingModule({ + providers: [ + provideNoopAnimations(), + { provide: GpEditService, useValue: mockGpEditService } + ], + imports: [GpEditComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(GpEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should load GPs on initialization', () => { + expect(mockGpEditService.getUpcomingGps).toHaveBeenCalled(); + expect(component.gps().length).toBe(1); + expect(component.gps()[0].track_name).toBe('Monaco'); + }); + + it('should load tracks on initialization', () => { + expect(mockGpEditService.getAllTracks).toHaveBeenCalled(); + expect(component.tracks().length).toBe(2); + }); + + it('should format dates correctly for datetime-local input', () => { + expect(component.gps()[0].date).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/); + }); + + it('should set loading state when loading data', () => { + // Setup async observable to simulate real API call + mockGpEditService.getUpcomingGps.and.returnValue( + of({ success: true, data: mockGpData }).pipe(delay(10)) + ); + + component.loading.set(false); + component.loadData(); + + // Check loading state immediately after calling loadData + expect(component.loading()).toBe(true); + }); + + it('should handle error when loading GPs', () => { + mockGpEditService.getUpcomingGps.and.returnValue(throwError(() => new Error('Load error'))); + + component.loadData(); + fixture.detectChanges(); + + expect(component.loading()).toBe(false); + }); + + it('should add toast correctly', () => { + component.addToast('Test Title', 'Test Message', 'success'); + + expect(component.toasts().length).toBe(1); + expect(component.toasts()[0].title).toBe('Test Title'); + expect(component.toasts()[0].message).toBe('Test Message'); + expect(component.toasts()[0].color).toBe('success'); + }); + + it('should remove toast when visibility changes to false', () => { + const toast = { title: 'Test', message: 'Message', color: 'info' }; + component.toasts.set([toast]); + + component.onToastVisibleChange(false, toast); + + expect(component.toasts().length).toBe(0); + }); + + it('should show confirmation modal with correct data', () => { + const testAction = jasmine.createSpy('testAction'); + + component.showConfirmation('Test Title', 'Test Message', testAction); + + expect(component.modalVisible()).toBe(true); + expect(component.modalTitle()).toBe('Test Title'); + expect(component.modalMessage()).toBe('Test Message'); + }); + + it('should execute pending action on confirm', () => { + const testAction = jasmine.createSpy('testAction'); + component.showConfirmation('Title', 'Message', testAction); + + component.onConfirmAction(); + + expect(testAction).toHaveBeenCalled(); + expect(component.modalVisible()).toBe(false); + }); + + it('should clear pending action on cancel', () => { + const testAction = jasmine.createSpy('testAction'); + component.showConfirmation('Title', 'Message', testAction); + + component.onCancelAction(); + + expect(testAction).not.toHaveBeenCalled(); + expect(component.modalVisible()).toBe(false); + }); + + it('should not update if bulk form is invalid', () => { + component.bulkForm.patchValue({ daysOffset: null }); + + component.onBulkUpdate(); + + expect(mockGpEditService.bulkUpdateGpDate).not.toHaveBeenCalled(); + }); + + it('should not update if days offset is zero', () => { + component.bulkForm.patchValue({ daysOffset: 0 }); + + component.onBulkUpdate(); + + expect(mockGpEditService.bulkUpdateGpDate).not.toHaveBeenCalled(); + }); + + it('should show confirmation before bulk update', () => { + spyOn(component, 'showConfirmation'); + component.bulkForm.patchValue({ daysOffset: 5 }); + + component.onBulkUpdate(); + + expect(component.showConfirmation).toHaveBeenCalled(); + }); + + it('should reload data after successful bulk update', (done) => { + spyOn(component, 'loadData'); + component.bulkForm.patchValue({ daysOffset: 5 }); + + // Manually call executeBulkUpdate to bypass confirmation + component['executeBulkUpdate'](5); + + setTimeout(() => { + expect(mockGpEditService.bulkUpdateGpDate).toHaveBeenCalledWith(5); + expect(component.loadData).toHaveBeenCalled(); + done(); + }, 100); + }); + + it('should handle error during bulk update', (done) => { + mockGpEditService.bulkUpdateGpDate.and.returnValue(throwError(() => new Error('Update error'))); + + component['executeBulkUpdate'](5); + + setTimeout(() => { + expect(component.loading()).toBe(false); + expect(component.toasts().some(t => t.color === 'danger')).toBe(true); + done(); + }, 100); + }); + + it('should not create GP if form is invalid', () => { + component.createForm.patchValue({ track_id: null, date: '' }); + + component.onCreate(); + + expect(mockGpEditService.createGp).not.toHaveBeenCalled(); + }); + + it('should create GP with correct data', (done) => { + component.createForm.patchValue({ + track_id: 1, + date: '2024-06-01T14:00', + has_sprint: true, + has_x2: false + }); + + component.onCreate(); + + setTimeout(() => { + expect(mockGpEditService.createGp).toHaveBeenCalledWith(jasmine.objectContaining({ + track_id: 1, + has_sprint: true, + has_x2: false + })); + done(); + }, 100); + }); + + it('should reset form after successful GP creation', (done) => { + component.createForm.patchValue({ + track_id: 1, + date: '2024-06-01T14:00', + has_sprint: true, + has_x2: false + }); + + component.onCreate(); + + setTimeout(() => { + expect(component.createForm.value.has_sprint).toBe(false); + expect(component.createForm.value.has_x2).toBe(false); + done(); + }, 100); + }); + + it('should show confirmation before deleting GP', () => { + spyOn(component, 'showConfirmation'); + + component.onDelete(1); + + expect(component.showConfirmation).toHaveBeenCalled(); + }); + + it('should delete GP and reload data', (done) => { + spyOn(component, 'loadData'); + + component['executeDelete'](1); + + setTimeout(() => { + expect(mockGpEditService.deleteGp).toHaveBeenCalledWith(1); + expect(component.loadData).toHaveBeenCalled(); + done(); + }, 100); + }); + + it('should update GP with correct data', (done) => { + const gp = { + id: 1, + track_id: 1, + track_name: 'Monaco', + date: '2024-05-26T14:00', + has_sprint: false, + has_x2: true + }; + + component.onSave(gp); + + setTimeout(() => { + expect(mockGpEditService.updateGp).toHaveBeenCalledWith(1, jasmine.objectContaining({ + has_sprint: false, + has_x2: true + })); + done(); + }, 100); + }); + + it('should show success toast after saving GP', (done) => { + const gp = { + id: 1, + track_id: 1, + track_name: 'Monaco', + date: '2024-05-26T14:00', + has_sprint: false, + has_x2: false + }; + + component.onSave(gp); + + setTimeout(() => { + expect(component.toasts().some(t => t.color === 'success')).toBe(true); + done(); + }, 100); + }); + + it('should handle error when saving GP', (done) => { + mockGpEditService.updateGp.and.returnValue(throwError(() => new Error('Save error'))); + const gp = { + id: 1, + track_id: 1, + track_name: 'Monaco', + date: '2024-05-26T14:00', + has_sprint: false, + has_x2: false + }; + + component.onSave(gp); + + setTimeout(() => { + expect(component.toasts().some(t => t.color === 'danger')).toBe(true); + done(); + }, 100); + }); + + it('should clear pending action when modal visibility changes to false', () => { + const testAction = jasmine.createSpy('testAction'); + component.showConfirmation('Title', 'Message', testAction); + + component.onModalVisibleChange(false); + + expect(component.modalVisible()).toBe(false); + }); +}); diff --git a/client/src/app/views/admin/gp-edit/gp-edit.component.ts b/client/src/app/views/admin/gp-edit/gp-edit.component.ts new file mode 100644 index 000000000..dbe45c039 --- /dev/null +++ b/client/src/app/views/admin/gp-edit/gp-edit.component.ts @@ -0,0 +1,267 @@ +import { Component, OnInit, inject, ChangeDetectionStrategy, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { + ContainerComponent, + RowComponent, + ColComponent, + CardComponent, + CardHeaderComponent, + CardBodyComponent, + TableDirective, + ButtonDirective, + ButtonCloseDirective, + FormModule, + GridModule, + SpinnerComponent, + ToasterComponent, + ToastComponent, + ToastHeaderComponent, + ToastBodyComponent, + ModalComponent, + ModalHeaderComponent, + ModalTitleDirective, + ModalBodyComponent, + ModalFooterComponent +} from '@coreui/angular'; +import { GpEditService } from '../../../service/gp-edit.service'; +import type { GPEditItem } from '@f123dashboard/shared'; + +interface Toast { + title: string; + message: string; + color: string; +} + +type GPEditViewModel = Omit & { date: string }; + +@Component({ + selector: 'app-gp-edit', + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + ContainerComponent, + RowComponent, + ColComponent, + CardComponent, + CardHeaderComponent, + CardBodyComponent, + TableDirective, + ButtonDirective, + ButtonCloseDirective, + FormModule, + GridModule, + SpinnerComponent, + ToasterComponent, + ToastComponent, + ToastHeaderComponent, + ToastBodyComponent, + ModalComponent, + ModalHeaderComponent, + ModalTitleDirective, + ModalBodyComponent, + ModalFooterComponent + ], + templateUrl: './gp-edit.component.html', + styleUrls: ['./gp-edit.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class GpEditComponent implements OnInit { + private gpEditService = inject(GpEditService); + private fb = inject(FormBuilder); + + gps = signal([]); + tracks = signal<{ id: number; name: string }[]>([]); + loading = signal(false); + + // Toaster state + toasts = signal([]); + + // Modal state + modalVisible = signal(false); + modalTitle = signal(''); + modalMessage = signal(''); + private pendingAction = signal<(() => void) | null>(null); + + // Bulk Update Form + bulkForm = this.fb.group({ + daysOffset: [0, [Validators.required]] + }); + + // Create GP Form + createForm = this.fb.group({ + track_id: [null as number | null, [Validators.required]], + date: ['', [Validators.required]], + has_sprint: [false], + has_x2: [false] + }); + + ngOnInit(): void { + this.loadData(); + this.loadTracks(); + } + + loadData(): void { + this.loading.set(true); + this.gpEditService.getUpcomingGps().subscribe({ + next: (res) => { + this.gps.set(res.data.map((gp: GPEditItem) => ({ + ...gp, + // Format for datetime-local input: "YYYY-MM-DDTHH:mm" + // Using toISOString() and slicing to get the correct format + date: new Date(gp.date).toISOString().slice(0, 16) + }))); + this.loading.set(false); + }, + error: (err) => { + console.error('Error loading GPs', err); + this.loading.set(false); + } + }); + } + + loadTracks(): void { + this.gpEditService.getAllTracks().subscribe({ + next: (res) => { + this.tracks.set(res.data); + }, + error: (err) => console.error('Error loading tracks', err) + }); + } + + addToast(title: string, message: string, color = 'success') { + this.toasts.update(toasts => [...toasts, { title, message, color }]); + } + + onToastVisibleChange(visible: boolean, toast: Toast) { + if (!visible) { + this.toasts.update(toasts => toasts.filter(t => t !== toast)); + } + } + + showConfirmation(title: string, message: string, action: () => void) { + this.modalTitle.set(title); + this.modalMessage.set(message); + this.pendingAction.set(action); + this.modalVisible.set(true); + } + + onConfirmAction() { + const action = this.pendingAction(); + if (action) { + action(); + } + this.modalVisible.set(false); + this.pendingAction.set(null); + } + + onCancelAction() { + this.modalVisible.set(false); + this.pendingAction.set(null); + } + + onModalVisibleChange(event: boolean) { + this.modalVisible.set(event); + if (!event) { + this.pendingAction.set(null); + } + } + + onBulkUpdate(): void { + if (this.bulkForm.invalid) return; + const daysOffset = this.bulkForm.value.daysOffset || 0; + if (daysOffset === 0) return; + + this.showConfirmation( + 'Conferma Spostamento', + `Sei sicuro di voler spostare tutti i prossimi GP di ${daysOffset} giorni?`, + () => this.executeBulkUpdate(daysOffset) + ); + } + + private executeBulkUpdate(daysOffset: number): void { + this.loading.set(true); + this.gpEditService.bulkUpdateGpDate(daysOffset).subscribe({ + next: (res) => { + if (res.success) { + this.loadData(); + this.addToast('Successo', 'Date aggiornate con successo!', 'success'); + } + }, + error: (err) => { + console.error('Error in bulk update', err); + this.loading.set(false); + this.addToast('Errore', 'Errore durante l\'aggiornamento di massa.', 'danger'); + } + }); + } + + onCreate(): void { + if (this.createForm.invalid) { return; } + const val = this.createForm.value; + + this.loading.set(true); + this.gpEditService.createGp({ + track_id: val.track_id!, + date: new Date(val.date!).toISOString(), // Ensure ISO string + has_sprint: val.has_sprint || false, + has_x2: val.has_x2 || false + }).subscribe({ + next: (res) => { + if (res.success) { + this.loadData(); + this.createForm.reset({ has_sprint: false, has_x2: false }); + this.addToast('Successo', 'GP creato con successo!', 'success'); + } + }, + error: (err) => { + console.error('Error creating GP', err); + this.loading.set(false); + this.addToast('Errore', 'Errore durante la creazione del GP.', 'danger'); + } + }); + } + + onDelete(id: number): void { + this.showConfirmation( + 'Conferma Eliminazione', + 'Sei sicuro di voler eliminare questo GP?', + () => this.executeDelete(id) + ); + } + + private executeDelete(id: number): void { + this.gpEditService.deleteGp(id).subscribe({ + next: (res) => { + if (res.success) { + this.loadData(); + this.addToast('Eliminato', 'GP eliminato con successo.', 'info'); + } + }, + error: (err) => { + console.error('Error deleting GP', err); + this.addToast('Errore', 'Errore durante l\'eliminazione.', 'danger'); + } + }); + } + + onSave(gp: GPEditViewModel): void { + + this.gpEditService.updateGp(gp.id, { + date: new Date(gp.date).toISOString(), // Ensure ISO format + has_sprint: gp.has_sprint, + has_x2: gp.has_x2 + }).subscribe({ + next: (res) => { + if (res.success) { + this.addToast('Salvato', 'Modifiche salvate con successo!', 'success'); + } + }, + error: (err) => { + console.error('Error updating GP', err); + this.addToast('Errore', 'Errore durante il salvataggio.', 'danger'); + } + }); + } +} diff --git a/client/src/app/views/admin/routes.ts b/client/src/app/views/admin/routes.ts new file mode 100644 index 000000000..b329ca062 --- /dev/null +++ b/client/src/app/views/admin/routes.ts @@ -0,0 +1,31 @@ +import { Routes } from '@angular/router'; + +export const routes: Routes = [ + { + path: '', + redirectTo: 'result-edit', + pathMatch: 'full' + }, + { + path: 'result-edit', + loadComponent: () => import('./admin.component').then(m => m.AdminComponent), + data: { + title: $localize`Admin` + } + }, + { + path: 'gp-edit', + loadComponent: () => import('./gp-edit/gp-edit.component').then(m => m.GpEditComponent), + data: { + title: 'GP Edit' + } + }, + { + path: 'change-password', + loadComponent: () => import('../admin-change-password/admin-change-password.component').then(m => m.AdminChangePasswordComponent), + data: { + title: 'Modifica Password Utente' + } + } +]; + diff --git a/client/src/app/views/albo-d-oro/albo-d-oro.component.html b/client/src/app/views/albo-d-oro/albo-d-oro.component.html new file mode 100644 index 000000000..f0adb8347 --- /dev/null +++ b/client/src/app/views/albo-d-oro/albo-d-oro.component.html @@ -0,0 +1,33 @@ +
+
+ @for (season of seasons(); track season.id) { +
+ @if (season.isLoading) { + + +
+ Loading... +
+
+
+ } @else { + + + } +
+ +
+ + +
+ } +
+
diff --git a/client/src/app/views/albo-d-oro/albo-d-oro.component.scss b/client/src/app/views/albo-d-oro/albo-d-oro.component.scss new file mode 100644 index 000000000..08340ef6a --- /dev/null +++ b/client/src/app/views/albo-d-oro/albo-d-oro.component.scss @@ -0,0 +1,50 @@ +:host { + display: block; +} + +.albo-page { + padding: 1.5rem 0; +} + +.albo-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1.5rem; + align-items: start; +} + +.albo-slot { + min-width: 0; +} + +.loading-card { + display: flex; + aspect-ratio: 1500 / 1861; + align-items: center; + justify-content: center; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 0; + background: + linear-gradient(180deg, rgba(0, 0, 0, 0.08) 0%, rgba(0, 0, 0, 0.3) 100%), + url("/assets/images/albo d'oro/sfondo.png") center/100% 100% no-repeat; +} + +@media (max-width: 1199px) { + .albo-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 767px) { + .albo-page { + padding: 1rem 0; + } + + .albo-grid { + gap: 1rem; + } + + .loading-card { + border-radius: 0; + } +} diff --git a/client/src/app/views/albo-d-oro/albo-d-oro.component.spec.ts b/client/src/app/views/albo-d-oro/albo-d-oro.component.spec.ts new file mode 100644 index 000000000..c545f47ba --- /dev/null +++ b/client/src/app/views/albo-d-oro/albo-d-oro.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; + +import { AlboDOroComponent } from './albo-d-oro.component'; + +describe('AlboDOroComponent', () => { + let component: AlboDOroComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [provideNoopAnimations(), ], + imports: [AlboDOroComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AlboDOroComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/views/albo-d-oro/albo-d-oro.component.ts b/client/src/app/views/albo-d-oro/albo-d-oro.component.ts new file mode 100644 index 000000000..6450b54f8 --- /dev/null +++ b/client/src/app/views/albo-d-oro/albo-d-oro.component.ts @@ -0,0 +1,149 @@ +import { Component, OnInit, inject, signal, computed, ChangeDetectionStrategy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { CardModule } from '@coreui/angular'; +import { PodiumCardComponent } from '../../components/podium-card/podium-card.component'; +import { DbDataService } from '../../service/db-data.service'; +import type { DriverData } from '@f123dashboard/shared'; + +interface PodiumEntry { + nome: string; + img: string; + colore: string; + punti: string; +} + +interface ClassificaEntry { + nome: string; + punti: string; +} + +interface SeasonDriverState { + isLoading: boolean; + podio: PodiumEntry[]; + classifica: ClassificaEntry[]; +} + +interface SeasonConfig { + id: number; + driverTitle: string; + fantaTitle: string; + fantaPodio: PodiumEntry[]; + fantaClassifica: ClassificaEntry[]; +} + +interface SeasonDisplay extends SeasonConfig { + isLoading: boolean; + driverPodio: PodiumEntry[]; + driverClassifica: ClassificaEntry[]; +} + +const PODIUM_SIZE = 3; +const DRIVER_AVATAR_PATH = '/assets/images/avatars'; +const FANTA_AVATAR_PATH = '/assets/images/avatars_fanta'; + +const SEASONS: SeasonConfig[] = [ + { + id: 2, + driverTitle: 'Campionato piloti 2025-26', + fantaTitle: 'Fanta 2025-26', + fantaPodio: [ + { nome: 'chichi', punti: '816', img: `${FANTA_AVATAR_PATH}/chichi.jpg`, colore: '#008080' }, + { nome: 'BoxBoxBuddy', punti: '798', img: `${FANTA_AVATAR_PATH}/BoxBoxBuddy.png`, colore: '#ff0000ff' }, + { nome: 'propriogiotto', punti: '793', img: `${FANTA_AVATAR_PATH}/propriogiotto.png`, colore: '#f699cd' } + + ], + fantaClassifica: [ + { nome: 'ferrari', punti: '410' }, + { nome: 'Togliattigrad', punti: '185' }, + { nome: 'Benver07', punti: '140' }, + { nome: 'shika', punti: '127' }, + { nome: 'Fambler', punti: '96' }, + { nome: 'kabubi', punti: '86' }, + { nome: 'Redmambalover99', punti: '49' }, + ], + }, + { + id: 1, + driverTitle: 'Campionato piloti 2024-25', + fantaTitle: 'Fanta 2024-25', + fantaPodio: [ + { nome: 'ProprioGiotto', punti: '499', img: `${FANTA_AVATAR_PATH}/7.png`, colore: '#f699cd' }, + { nome: 'Chichi', punti: '481', img: `${FANTA_AVATAR_PATH}/chichi.jpg`, colore: '#008080' }, + { nome: 'Fambler', punti: '480', img: `${FANTA_AVATAR_PATH}/2.png`, colore: '#ff0000ff' }, + ], + fantaClassifica: [ + { nome: 'Shika', punti: '472' }, + { nome: 'Matte', punti: '432' }, + { nome: 'Ali', punti: '414' }, + { nome: 'Sara', punti: '386' }, + { nome: 'BoxBoxBunny', punti: '381' }, + { nome: 'Omix', punti: '347' }, + { nome: 'GommaRosa', punti: '304' }, + ], + }, +]; + +@Component({ + selector: 'app-albo-d-oro', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, PodiumCardComponent, CardModule], + templateUrl: './albo-d-oro.component.html', + styleUrls: ['./albo-d-oro.component.scss'] +}) +export class AlboDOroComponent implements OnInit { + private dbDataService = inject(DbDataService); + + private driverStates = signal>( + Object.fromEntries(SEASONS.map(s => [s.id, { isLoading: true, podio: [], classifica: [] }])) + ); + + seasons = computed(() => + SEASONS.map(config => ({ + ...config, + isLoading: this.driverStates()[config.id].isLoading, + driverPodio: this.driverStates()[config.id].podio, + driverClassifica: this.driverStates()[config.id].classifica, + })) + ); + + async ngOnInit(): Promise { + await Promise.all(SEASONS.map(s => this.loadChampionship(s.id))); + } + + private async loadChampionship(seasonId: number): Promise { + try { + const drivers = await this.dbDataService.getDriversBySeason(seasonId); + const sorted = [...drivers].sort((a, b) => +b.total_points - +a.total_points); + this.driverStates.update(states => ({ + ...states, + [seasonId]: { isLoading: false, podio: this.buildPodium(sorted), classifica: this.buildClassifica(sorted) }, + })); + } catch (error) { + console.error(`Error loading season ${seasonId} drivers:`, error); + this.driverStates.update(states => ({ + ...states, + [seasonId]: { ...states[seasonId], isLoading: false } + })); + } + } + + private buildPodium(drivers: DriverData[]): PodiumEntry[] { + return drivers.slice(0, PODIUM_SIZE).map(driver => this.toPodiumEntry(driver)); + } + + private toPodiumEntry(driver: DriverData): PodiumEntry { + return { + nome: driver.driver_username, + img: `${DRIVER_AVATAR_PATH}/${driver.driver_username}.png`, + colore: driver.driver_color, + punti: driver.total_points.toString() + }; + } + + private buildClassifica(drivers: DriverData[]): ClassificaEntry[] { + return drivers.slice(PODIUM_SIZE).map(driver => ({ + nome: driver.driver_username, + punti: driver.total_points.toString() + })); + } +} diff --git a/client/src/app/views/albo-d-oro/routes.ts b/client/src/app/views/albo-d-oro/routes.ts new file mode 100644 index 000000000..93965c0cb --- /dev/null +++ b/client/src/app/views/albo-d-oro/routes.ts @@ -0,0 +1,12 @@ +import { Routes } from '@angular/router'; +import { AlboDOroComponent } from './albo-d-oro.component'; + +export const routes: Routes = [ + { + path: '', + component: AlboDOroComponent, + data: { + title: 'Albo D\'Oro' + } + } +]; diff --git a/client/src/app/views/championship/championship.component.html b/client/src/app/views/championship/championship.component.html new file mode 100644 index 000000000..bc9600f33 --- /dev/null +++ b/client/src/app/views/championship/championship.component.html @@ -0,0 +1,107 @@ + + @for (gp of championship_data; track $index) { + + + + + + +
+ + {{gp.track_name}} + +
+
+ @if (gp.gran_prix_has_sprint == 1) { + + Sprint + + } + @if (gp.gran_prix_has_x2 == 1) { + + Points X2 + + } + +
{{gp.gran_prix_date | date: 'dd/MM/yyyy'}}
+
+
+
+
+ @if (isDone(gp)) { + +
+ + + + + + @if (gp.gran_prix_has_sprint == 1) { + + } + + + + + + + @for (position of getAllPositions(gp); track position) { + + + + @if (gp.gran_prix_has_sprint == 1) { + + } + + + + } + + + @if (getActiveFastLapDriver(gp) || (gp.gran_prix_has_sprint == 1 && gp.fastLapDrivers.sprint)) { + + + + @if (gp.gran_prix_has_sprint == 1) { + + } + + + + } + + + @if (getDNFDrivers(getActiveRaceSession(gp)) || (gp.gran_prix_has_sprint == 1 && getDNFDrivers(gp.sessions.sprint))) { + + + + @if (gp.gran_prix_has_sprint == 1) { + + } + + + + } + +
#GaraSprintQualificaProve Libere
+ @if (position <= 3) { + + } @else { + {{position}} + } + {{getDriverByPosition(getActiveRaceSession(gp), position)}}{{getDriverByPosition(gp.sessions.sprint, position)}}{{getDriverByPosition(gp.sessions.qualifying, position)}}{{getDriverByPosition(gp.sessions.free_practice, position)}}
+ GV + + {{getActiveFastLapDriver(gp)}}{{gp.fastLapDrivers.sprint || ''}}
DNF{{getDNFDrivers(getActiveRaceSession(gp))}}{{getDNFDrivers(gp.sessions.sprint)}}
+
+
+ } +
+
+ } +
+ \ No newline at end of file diff --git a/client/src/app/views/championship/championship.component.scss b/client/src/app/views/championship/championship.component.scss new file mode 100644 index 000000000..f8753ede3 --- /dev/null +++ b/client/src/app/views/championship/championship.component.scss @@ -0,0 +1,22 @@ +/* Mobile optimization for table */ +@media (max-width: 767.98px) { + .table-responsive { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + + table { + th, td { + white-space: wrap; + vertical-align: middle; + padding: 0.3rem; + font-size: 0.8rem; + } + } + } +} + +/* Ensure medal icons are consistent */ +img.cIcon { + width: 25px; + vertical-align: middle; +} diff --git a/client/src/app/views/championship/championship.component.spec.ts b/client/src/app/views/championship/championship.component.spec.ts new file mode 100644 index 000000000..6df894402 --- /dev/null +++ b/client/src/app/views/championship/championship.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; +import { CardModule, GridModule, TableModule, UtilitiesModule } from '@coreui/angular'; +import { IconSetService } from '@coreui/icons-angular'; +import { iconSubset } from '../../icons/icon-subset'; + +import { ChampionshipComponent } from './championship.component'; + +describe('ChampionshipComponent', () => { + let component: ChampionshipComponent; + let fixture: ComponentFixture; + let iconSetService: IconSetService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [GridModule, CardModule, TableModule, GridModule, UtilitiesModule, RouterTestingModule, ChampionshipComponent], + providers: [provideNoopAnimations(), IconSetService] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ChampionshipComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + beforeEach(() => { + iconSetService = TestBed.inject(IconSetService); + iconSetService.icons = { ...iconSubset }; + + fixture = TestBed.createComponent(ChampionshipComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/client/src/app/views/championship/championship.component.ts b/client/src/app/views/championship/championship.component.ts new file mode 100644 index 000000000..45c63452e --- /dev/null +++ b/client/src/app/views/championship/championship.component.ts @@ -0,0 +1,118 @@ +import { Component, OnInit, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { BadgeComponent, RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, TableDirective, TableColorDirective, TableActiveDirective, BorderDirective, AlignDirective, ContainerComponent } from '@coreui/angular'; +import { cifBh, cifAt, cifMc, cifJp, cifHu, cifCn, cifCa, cifEs, cifGb, cifBe, cifNl, cifAz, cifSg, cifIt, cifUs, cifAu, cifMx, cifBr, cifQa, cifAe, cifSa } from '@coreui/icons'; +import { cilFire } from '@coreui/icons'; +import { IconDirective } from '@coreui/icons-angular'; +import { DbDataService } from '../../service/db-data.service'; +import { allFlags } from '../../model/constants'; +import type { ChampionshipData, SessionResult } from '@f123dashboard/shared'; + +@Component({ + selector: 'app-championship', + imports: [BadgeComponent, ContainerComponent, IconDirective, CommonModule, IconDirective, RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, TableDirective], + templateUrl: './championship.component.html', + styleUrl: './championship.component.scss' +}) +export class ChampionshipComponent implements OnInit{ + private dbData = inject(DbDataService); + + + public championship_data: ChampionshipData[] = []; + public allFlags = allFlags; + + + public fireIcon: string[] = cilFire; + + ngOnInit(): void { + this.championship_data = this.dbData.championship(); + } + + // Helper method to get driver by position in a session + getDriverByPosition(session: SessionResult[] | undefined, position: number): string { + if (!session) {return '';} + const result = session.find(r => r.position === position); + return result?.driver_username || ''; + } + + // Helper method to get DNF drivers + getDNFDrivers(session: SessionResult[] | undefined): string { + if (!session) {return '';} + const dnfDrivers = session.filter(r => r.position === 0); + return dnfDrivers.map(d => d.driver_username).join(', '); + } + + // Helper method to get the active race session (race or full_race based on has_x2) + getActiveRaceSession(gp: ChampionshipData): SessionResult[] | undefined { + return gp.gran_prix_has_x2 === 1 ? gp.sessions.full_race : gp.sessions.race; + } + + // Helper method to get fast lap driver for active race session + getActiveFastLapDriver(gp: ChampionshipData): string { + const sessionType = gp.gran_prix_has_x2 === 1 ? 'full_race' : 'race'; + return gp.fastLapDrivers[sessionType] || ''; + } + + // Helper method to check if session has any results + hasSessionResults(session: SessionResult[] | undefined): boolean { + return !!(session && session.length > 0); + } + + // Helper method to get sorted results (excluding DNFs for position display) + getSortedResults(session: SessionResult[] | undefined): SessionResult[] { + if (!session) {return [];} + return session + .filter(result => result.position > 0) // Exclude DNFs (position 0) + .sort((a, b) => a.position - b.position); + } + + // Helper method to get all possible positions across all sessions + getAllPositions(gp: ChampionshipData): number[] { + const positions = new Set(); + + // Get positions from all sessions + const sessions = [ + gp.sessions.race, + gp.sessions.full_race, + gp.sessions.sprint, + gp.sessions.qualifying, + gp.sessions.free_practice + ]; + + sessions.forEach(session => { + if (session) + {session.forEach(result => { + if (result.position > 0) // Exclude DNFs + {positions.add(result.position);} + + });} + + }); + + // Convert to sorted array + return Array.from(positions).sort((a, b) => a - b); + } + + // Helper method to get position styling + getPositionStyle(position: number): Record { + if (position === 1) {return { 'color': 'green' };} + if (position === 8) {return { 'color': 'red' };} + // No styling for other positions (was previously black) + return {}; + } + + // Helper method to get position name for medal images + getPositionName(position: number): string { + switch (position) { + case 1: return 'first'; + case 2: return 'second'; + case 3: return 'third'; + default: return ''; + } + } + isDone(gp: ChampionshipData): boolean { + return gp.sessions.race !== undefined || + gp.sessions.full_race !== undefined || + gp.sessions.qualifying !== undefined; + } +} \ No newline at end of file diff --git a/client/src/app/views/championship/routes.ts b/client/src/app/views/championship/routes.ts new file mode 100644 index 000000000..676fc37d5 --- /dev/null +++ b/client/src/app/views/championship/routes.ts @@ -0,0 +1,12 @@ +import { Routes } from '@angular/router'; + +export const routes: Routes = [ + { + path: '', + loadComponent: () => import('./championship.component').then(m => m.ChampionshipComponent), + data: { + title: $localize`Championship` + } + } +]; + diff --git a/client/src/app/views/credits/credits.component.html b/client/src/app/views/credits/credits.component.html new file mode 100644 index 000000000..477f1999a --- /dev/null +++ b/client/src/app/views/credits/credits.component.html @@ -0,0 +1,101 @@ + + +

Il Team RaceForFederica 🛠️

+
+ + + + +
+ 💰 + Soldi raccolti: + € 0 +
+
+
+ + + + + + + + Paolo Celada's avatar + +
Paolo Celada
+ CEO - Frontend Developer - Social Media Manager + +
+
+
+ + + Federico Degioanni's avatar + +
Federico Degioanni
+ CFO - Graphics Designer + +
+
+
+ + + Andrea Dominici's avatar + +
Andrea Dominici
+ CTO - Full Stack Developer + +
+
+
+
+
+ + +
+ + +
    +
  • Organizzare un campionato, mantenere una pagina Instagram e creare un sito richiede tempo, sudore e tante best****e 🤬
  • +
  • I premi costano e gli sponsor ancora non si vedono 😴
  • +
  • Se vi fa piacere supportate questo progettino con il pulsante qui sotto, che siano 1, 2 o MILLE MILIONI di euro 🤑
  • +
+
+
+ + + + + + + + + diff --git a/client/src/app/views/credits/credits.component.scss b/client/src/app/views/credits/credits.component.scss new file mode 100644 index 000000000..fc7212ede --- /dev/null +++ b/client/src/app/views/credits/credits.component.scss @@ -0,0 +1,93 @@ +/* SATISPAY BUTTON */ +.satispay-container { + display: flex; + justify-content: center; +} + +.satispay-icon-wrapper { + background-color: white; + border-radius: 50%; + padding: 6px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 20px; + width: 45px; + height: 45px; + } + +.satispay-button { + display: flex; + align-items: center; + background-color: #ef3e43; + color: white; + text-decoration: none; + border-radius: 999px; + padding: 20px 20px; + font-family: sans-serif; + font-size: 18px; + box-sizing: border-box; + max-width: fit-content; + } + +.satispay-button:hover { + background-color: #d7383d; + } + +.satispay-icon { + width: 38px; + height: 38px; + } + +/* SPACE BETWEEN ROWS */ +.row-spacing { + margin-top: 2rem; /* or any consistent spacing unit */ + } + + +/* GITHUB BUTTON */ +.github-logo { + height: 1em; /* matches the text size */ + width: auto; + vertical-align: middle; +} + +.github-link { + color: white; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 6px; +} + +/* MONEY RAISED */ +.money-counter-wrapper { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 16px; + background: #f8f9fa; + border-radius: 8px; + font-family: 'Segoe UI', sans-serif; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); + max-width: fit-content; + margin: 20px auto; +} + +.money-counter-wrapper .money-emoji { + font-size: 20px; +} + +.money-counter-wrapper .money-label { + font-size: 16px; + color: #444; +} + +.money-counter-wrapper .money-amount { + font-size: 18px; + color: #28a745; + font-weight: 600; + min-width: 80px; + text-align: right; +} \ No newline at end of file diff --git a/client/src/app/views/credits/credits.component.spec.ts b/client/src/app/views/credits/credits.component.spec.ts new file mode 100644 index 000000000..d5cc3c2e8 --- /dev/null +++ b/client/src/app/views/credits/credits.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; + +import { CreditsComponent } from './credits.component'; + +describe('CreditsComponent', () => { + let component: CreditsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [provideNoopAnimations(), ], + imports: [CreditsComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CreditsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/views/credits/credits.component.ts b/client/src/app/views/credits/credits.component.ts new file mode 100644 index 000000000..4f537a86c --- /dev/null +++ b/client/src/app/views/credits/credits.component.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; +import { + CardBodyComponent, + CardComponent, + CardImgDirective, + CardTitleDirective, + ColComponent, + RowComponent, + ButtonDirective +} from '@coreui/angular' + +@Component({ + selector: 'app-credits', + imports: [ + CardComponent, + CardBodyComponent, + CardTitleDirective, + CardImgDirective, + ColComponent, + RowComponent, + ButtonDirective + ], + templateUrl: './credits.component.html', + styleUrl: './credits.component.scss' +}) +export class CreditsComponent { + +} diff --git a/client/src/app/views/credits/routes.ts b/client/src/app/views/credits/routes.ts new file mode 100644 index 000000000..ee1f5cde2 --- /dev/null +++ b/client/src/app/views/credits/routes.ts @@ -0,0 +1,12 @@ +import { Routes } from '@angular/router'; + +export const routes: Routes = [ + { + path: '', + loadComponent: () => import('./credits.component').then(m => m.CreditsComponent), + data: { + title: $localize`Credits` + } + } +]; + diff --git a/client/src/app/views/dashboard/dashboard-charts-data.ts b/client/src/app/views/dashboard/dashboard-charts-data.ts new file mode 100644 index 000000000..53010dc28 --- /dev/null +++ b/client/src/app/views/dashboard/dashboard-charts-data.ts @@ -0,0 +1,398 @@ +import { Injectable, inject } from '@angular/core'; +import { + ChartData, + ChartDataset, + ChartOptions, + ChartType, + ScaleOptions +} from 'chart.js'; + +import { getStyle } from '@coreui/utils'; +import { DbDataService } from '../../service/db-data.service'; +import type { CumulativePointsData } from '@f123dashboard/shared'; + +// Constants +const DEFAULT_CHART_SCALE = 500; +const MONTH_PERIOD_ELEMENTS = 12; +const ALL_PERIOD_ELEMENTS = 27; +const DEFAULT_MAX_RACES = 8; +const SCALE_ROUNDING = 100; +const DESKTOP_BREAKPOINT = 900; + +const DRIVER_COLORS = [ + '#8a2be2', '#32cd32', '#c0c0c0', '#f86c6b', '#ffa500', '#6495ed', + '#ff6384', '#36a2eb', '#ffce56', '#4bc0c0', '#9966ff', '#ff9f40' +]; + +interface Track { + name: string; + country: string; + [key: string]: any; +} + +type DriverDataMap = Record; + +export interface IChartProps { + data?: ChartData; + labels?: string[]; + options?: ChartOptions; + colors?: any; + type: ChartType; + legend?: any; + elements?: number; + [propName: string]: any; +} + +@Injectable({ + providedIn: 'any' +}) +export class DashboardChartsData { + private dbData = inject(DbDataService); + + public mainChart: IChartProps = { type: 'line' }; + public championshipTrend: CumulativePointsData[] = []; + public championshipTracks: Track[] = []; + + private chartScale: number = DEFAULT_CHART_SCALE; + + constructor() { + this.initMainChart(); + } + + + /** + * Initialize the main championship chart with cumulative points data. + * @param period - Chart period: 'Month' shows last N races, 'all' shows entire season + * @param maxNumberOfRaces - Maximum number of races to display in Month view + */ + initMainChart(period = 'all', maxNumberOfRaces: number = DEFAULT_MAX_RACES): void { + this.loadChartData(); + + const completedTracks = this.getCompletedTracks(); + const uniqueDrivers = this.getUniqueDrivers(); + + this.mainChart.elements = period === 'Month' ? MONTH_PERIOD_ELEMENTS : ALL_PERIOD_ELEMENTS; + + const { labels, driverData } = period === 'Month' + ? this.prepareMonthPeriodData(completedTracks, maxNumberOfRaces, uniqueDrivers) + : this.prepareAllPeriodData(completedTracks.length, uniqueDrivers); + + this.updateChartConfiguration(labels, driverData, uniqueDrivers); + } + + /** + * Load championship trend and track data from the database service. + */ + private loadChartData(): void { + this.championshipTrend = this.dbData.cumulativePoints(); + this.championshipTracks = this.dbData.tracks(); + } + + /** + * Get list of tracks that have completed races. + */ + private getCompletedTracks(): Track[] { + const completedTrackNames = new Set( + this.championshipTrend.map(item => item.track_name) + ); + + return this.championshipTracks.filter(track => + completedTrackNames.has(track.name) + ); + } + + /** + * Get unique list of driver usernames from championship data. + */ + private getUniqueDrivers(): string[] { + return [...new Set(this.championshipTrend.map(item => item.driver_username))]; + } + + /** + * Group championship data by driver username. + */ + private groupDataByDriver(): DriverDataMap { + const driverData: DriverDataMap = {}; + + for (const trendItem of this.championshipTrend) { + if (!driverData[trendItem.driver_username]) + {driverData[trendItem.driver_username] = [];} + + driverData[trendItem.driver_username].push(Number(trendItem.cumulative_points)); + } + + return driverData; + } + + /** + * Prepare data for the Month period view (showing last N races). + */ + private prepareMonthPeriodData( + completedTracks: Track[], + maxNumberOfRaces: number, + uniqueDrivers: string[] + ): { labels: string[], driverData: DriverDataMap } { + const tracksToUse = this.selectTracksForMonthView(completedTracks, maxNumberOfRaces); + const labels = tracksToUse.map(track => track.country); + + const driverData = this.groupDataByDriver(); + const processedDriverData = this.processMonthPeriodDriverData( + driverData, + completedTracks.length, + maxNumberOfRaces + ); + + return { labels, driverData: processedDriverData }; + } + + /** + * Select which tracks to display in Month view. + */ + private selectTracksForMonthView(completedTracks: Track[], maxNumberOfRaces: number): Track[] { + const numberOfCompletedRaces = completedTracks.length; + + if (numberOfCompletedRaces < maxNumberOfRaces) { + // Include upcoming races to fill the chart + const racesNeeded = maxNumberOfRaces - numberOfCompletedRaces; + const completedTrackNames = new Set(completedTracks.map(t => t.name)); + + const upcomingTracks = this.championshipTracks + .filter(track => !completedTrackNames.has(track.name)) + .slice(0, racesNeeded); + + return [...completedTracks, ...upcomingTracks]; + } else { + // Show only the last N completed races + const startIndex = Math.max(0, completedTracks.length - maxNumberOfRaces); + return completedTracks.slice(startIndex); + } + } + + /** + * Process driver data for Month view, calculating relative points from first race. + */ + private processMonthPeriodDriverData( + driverData: DriverDataMap, + numberOfCompletedRaces: number, + maxNumberOfRaces: number + ): DriverDataMap { + const processedData: DriverDataMap = {}; + let maxDriverValue = 0; + + for (const driver in driverData) { + // Reverse to get chronological order (oldest to newest) + const allData = driverData[driver].reverse(); + + // Select data slice to show + const dataToShow = this.selectDataSlice(allData, numberOfCompletedRaces, maxNumberOfRaces); + + // Calculate relative points from baseline (first value) + const baselineValue = dataToShow.find(v => v !== null) ?? 0; + const relativeData = dataToShow.map((value: number | null) => + value === null ? null : value - baselineValue + ); + + processedData[driver] = relativeData; + + // Track maximum value for chart scaling + const driverMax = this.getMaxValue(relativeData); + if (driverMax > maxDriverValue) + {maxDriverValue = driverMax;} + + } + + // Round chart scale up to nearest hundred + this.chartScale = Math.ceil(maxDriverValue / SCALE_ROUNDING) * SCALE_ROUNDING; + + return processedData; + } + + /** + * Select appropriate data slice based on completed and requested races. + */ + private selectDataSlice( + allData: (number | null)[], + numberOfCompletedRaces: number, + maxNumberOfRaces: number + ): (number | null)[] { + if (numberOfCompletedRaces < maxNumberOfRaces) { + // Pad with nulls for upcoming races + const paddingNeeded = maxNumberOfRaces - numberOfCompletedRaces; + const padding = new Array(paddingNeeded).fill(null); + return [...allData, ...padding]; + } else + // Take last N races + {return allData.slice(-maxNumberOfRaces);} + + } + + /** + * Get maximum non-null value from an array. + */ + private getMaxValue(data: (number | null)[]): number { + const completedValues = data.filter((v): v is number => v !== null); + return completedValues.length > 0 ? Math.max(...completedValues) : 0; + } + + /** + * Prepare data for the All period view (showing entire season). + */ + private prepareAllPeriodData( + numberOfCompletedRaces: number, + uniqueDrivers: string[] + ): { labels: string[], driverData: DriverDataMap } { + const labels = this.championshipTracks.map(track => track.country); + + const driverData = this.groupDataByDriver(); + const processedDriverData = this.processAllPeriodDriverData(driverData, numberOfCompletedRaces); + + return { labels, driverData: processedDriverData }; + } + + /** + * Process driver data for All view, padding with nulls for upcoming races. + */ + private processAllPeriodDriverData( + driverData: DriverDataMap, + numberOfCompletedRaces: number + ): DriverDataMap { + const processedData: DriverDataMap = {}; + const upcomingRacesCount = this.championshipTracks.length - numberOfCompletedRaces; + const padding = new Array(upcomingRacesCount).fill(null); + + for (const driver in driverData) { + // Reverse to get chronological order and pad for upcoming races + const completedData = driverData[driver].reverse(); + processedData[driver] = [...completedData, ...padding]; + } + + // Calculate max value and set chart scale + const maxTrendValue = Math.max( + ...this.championshipTrend.map(item => Number(item.cumulative_points)) + ); + this.chartScale = Math.ceil(maxTrendValue / SCALE_ROUNDING) * SCALE_ROUNDING; + + return processedData; + } + + /** + * Update the main chart configuration with processed data. + */ + private updateChartConfiguration( + labels: string[], + driverData: DriverDataMap, + uniqueDrivers: string[] + ): void { + // Store driver data in mainChart for Chart.js consumption + for (const driver of uniqueDrivers) + {this.mainChart[driver] = driverData[driver] || [];} + + + const datasets = this.createDatasets(uniqueDrivers); + const options = this.createChartOptions(); + + this.mainChart.type = 'line'; + this.mainChart.options = options; + this.mainChart.data = { + datasets, + labels + }; + } + + /** + * Create Chart.js datasets for each driver. + */ + private createDatasets(uniqueDrivers: string[]): ChartDataset[] { + return uniqueDrivers.map((driverUsername, index) => { + const color = DRIVER_COLORS[index % DRIVER_COLORS.length]; + + return { + data: this.mainChart[driverUsername] || [], + label: driverUsername, + backgroundColor: color, + borderColor: color, + pointHoverBackgroundColor: color, + borderWidth: 2, + }; + }); + } + + /** + * Create Chart.js options configuration. + */ + private createChartOptions(): ChartOptions { + const screenWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; + const scales = this.getScales(); + const pointRadius = this.calculatePointRadius(screenWidth); + + return { + maintainAspectRatio: false, + plugins: { + legend: { + display: true + } + }, + scales, + elements: { + line: { + tension: 0 + }, + point: { + radius: pointRadius.radius, + hitRadius: pointRadius.hitRadius, + hoverRadius: pointRadius.hoverRadius, + hoverBorderWidth: pointRadius.hoverBorderWidth + } + } + }; + } + + /** + * Calculate responsive point sizes based on screen width. + */ + private calculatePointRadius(screenWidth: number) { + const baseSize = screenWidth > DESKTOP_BREAKPOINT ? DESKTOP_BREAKPOINT : screenWidth; + + return { + radius: baseSize / 350, + hitRadius: baseSize / 200, + hoverRadius: baseSize / 250, + hoverBorderWidth: baseSize / 300 + }; + } + + /** + * Get Chart.js scale configuration for X and Y axes. + */ + getScales(): ScaleOptions { + const colorBorderTranslucent = getStyle('--cui-border-color-translucent'); + const colorBody = getStyle('--cui-body-color'); + + return { + x: { + grid: { + color: colorBorderTranslucent, + drawOnChartArea: false + }, + ticks: { + color: colorBody + } + }, + y: { + border: { + color: colorBorderTranslucent + }, + grid: { + color: colorBorderTranslucent + }, + max: this.chartScale, + beginAtZero: true, + ticks: { + color: colorBody, + maxTicksLimit: 5, + stepSize: Math.ceil(this.chartScale / 5) + } + } + }; + } +} diff --git a/client/src/app/views/dashboard/dashboard.component.html b/client/src/app/views/dashboard/dashboard.component.html new file mode 100644 index 000000000..225f57cb9 --- /dev/null +++ b/client/src/app/views/dashboard/dashboard.component.html @@ -0,0 +1,480 @@ +@if (isLive()) { + + + +

+ Live Stream +

+
+ +
+ +
+
+
+
+} + + + + + +

Classifica Campionato 🏅

+
+ + + + + + + + + +
+ + + + + + + @if (showColumn()) { + + } + @if (showColumn()) { + + } + + @if (showGainedPointsColumn()) { + + } + + + + @for (user of championship_standings_users(); track + user.driver_username; let i = $index) { + + + + + @if (showColumn()) { + + } + @if (showColumn()) { + + } + + @if (showGainedPointsColumn()) { + + } + + } + +
Pos. + + Username + Scuderia + + Pilota + Punti + +
+
#{{ i + 1 }}
+
+ + +
{{ user.driver_username }}
+
+ {{ getConstructorForDriver(user.driver_username) }} logo + +
+ {{ user.driver_name }} {{ user.driver_surname }} +
+
+
+ {{ user.total_points }} +
+
+
+ {{ user.gainedPoints }} +
+
+
+
+ + + + + + + + + + + @if (showGainedPointsColumn()) { + + } + + + + @for (constructor of constructors(); track + constructor.constructor_name; let i = $index) { + + + + + + + @if (showGainedPointsColumn()) { + + } + + } + +
Pos. + + + Scuderia + + Piloti + Punti + +
+
#{{ i + 1 }}
+
+ {{ constructor.constructor_name }} logo + +
{{ constructor.constructor_name }}
+
+
+ + +
+
+
+ {{ constructor.constructor_tot_points }} +
+
+
+ {{constructor.constructor_gained_points}} +
+
+
+
+
+
+
+
+ + + @if (championshipNextTracks().length > 0) { + + + + @for (next_track of championshipNextTracks(); track next_track.track_id) { + + + + + +

+ Prossimo GP: {{ next_track.name }} + +

+
+ @if (next_track.has_sprint == 1) { + + Sprint + + } @if (next_track.has_x2 == 1) { + + Points X2 + + } +
+
+ +
+ Track image for {{ next_track.name }} +
+ + +
+ + Date: {{ next_track.date }} +
+
+ +
+ + Fastest Lap: + {{ next_track.besttime_driver_time }} ({{ + next_track.username + }}) +
+
+
+
+
+
+ } +
+ + +
+ } + @else if (championshipNextTracks().length === 0) { + + +

Classifica Fanta

+
+ + + +
+ } +
+ + + + +

+ Pilota on Fire + 🔥 +

+
+ + {{ driverOfWeek().driver_username }}'s avatar +
+
+ {{ driverOfWeek().driver_username }}
+ {{ driverOfWeek().points }} Pt. +
+
+
+
+ + + +

+ Squadra on Fire + 🔥 +

+
+ + {{ constructorOfWeek().constructor_name }}'s logo +
+
+ {{ constructorOfWeek().constructor_name }}
+ {{ constructorOfWeek().points }} Pt. +
+
+
+
+
+ +
+
+ + +
+ + + + + + + + + + + + + + +
+ Dettagli Pilota +
+ + +
+ + @if (selectedPilot()) { +
+ + +
+ } +
+
+ + + + +
+ Dettagli Costruttore +
+ + +
+ + @if (selectedConstructor()) { +
+ + +
+ } +
+
+ + + + diff --git a/client/src/app/views/dashboard/dashboard.component.scss b/client/src/app/views/dashboard/dashboard.component.scss new file mode 100644 index 000000000..acd9b453e --- /dev/null +++ b/client/src/app/views/dashboard/dashboard.component.scss @@ -0,0 +1,167 @@ +:host { + .legend { + small { + font-size: x-small; + } + } +} + +.carousel-control-prev{ + filter: invert(1); +} +.carousel-control-next{ + filter: invert(1); +} + +.carousel-indicators{ + filter: invert(1); +} + +@media (max-width: 415px) { + .pilot { + display: none; + } + .carousel-height { + height: 400px; + } +} + +/* Modal animation with slide-in from bottom effect */ +@keyframes slideInFromBottom { + from { + opacity: 0; + transform: translateY(100%); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.modalAnimation { + animation: slideInFromBottom 0.7s ease-in-out; +} + + + +.carousel-inner img { + width: 100%; + height: 100%; + object-fit: cover; /* Mantiene le proporzioni ritagliando l'immagine se necessario */ +} +.carousel-height { + height: 500px; /* Imposta l'altezza desiderata per il carosello */ +} + +.table-responsive { + overflow: hidden !important; +} +.card-body{ + padding: 1vw; +} + +/* Shrink the card and tabs to content height */ +c-card, +c-tabs, +c-tab-panel { + height: auto !important; + min-height: unset !important; + flex: 0 0 auto !important; /* prevent stretching */ +} + +/* Optional: make tab content not add extra padding */ +c-card-body { + padding-bottom: 0.5rem; /* adjust as needed */ +} + +.custom-table { + border-collapse: separate; + border-spacing: 0; + overflow: hidden; /* Nasconde i bordi sporgenti */ + width: 100%; /* force table to use full width */ + table-layout: auto; /* or fixed, depending on how you want column widths */ + + td { + vertical-align: middle; + } +} + +.custom-c-tab { + padding: 0 !important; /* removes Bootstrap p-3 */ +} + +.table-wrapper { + width: 100%; + overflow-x: auto; /* if content overflows */ +} + + +/* STYLE FOR DRIVER OF THE WEEK AND CONSTRUCTOR OF THE WEEK */ +.card-avatar { + width: 100%; + max-width: 100px; + aspect-ratio: 1 / 1; + object-fit: contain; + margin: 0 auto; + display: block; + padding: 10px; + background: none !important; /* removes white background */ + box-shadow: none; +} + +/* Driver avatar - circular */ +.driver-avatar { + border-radius: 50%; + flex-shrink: 0; +} + +/* Constructor avatar - keep square */ +.constructor-avatar { + border-radius: 0; + flex-shrink: 0; +} + +/* Text box in purple */ +.info-box { + background: transparent; + display: inline-block; +} + +/* Mobile-friendly scaling */ +@media (max-width: 768px) { + .card-avatar { + max-width: 120px; + } + .info-box { + font-size: 0.9rem; + } +} + +.separator { + background-color: #ccc; /* grey color */ + flex-shrink: 0; +} + +/* Vertical by default (desktop) */ +@media (min-width: 768px) { + .separator { + width: 2px; + height: 60px; + } +} + +/* Horizontal on small screens */ +@media (max-width: 767px) { + .separator { + width: 60px; + height: 2px; + } +} + +@media (max-width: 495px) { + .on-fire-title { + display: flex; + flex-direction: column; + align-items: center; + } +} diff --git a/client/src/app/views/dashboard/dashboard.component.spec.ts b/client/src/app/views/dashboard/dashboard.component.spec.ts new file mode 100644 index 000000000..72009b7e9 --- /dev/null +++ b/client/src/app/views/dashboard/dashboard.component.spec.ts @@ -0,0 +1,553 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { DashboardComponent, DriverDataWithGainedPoints } from './dashboard.component'; +import { DbDataService } from '../../service/db-data.service'; +import { TwitchApiService } from '../../service/twitch-api.service'; +import { DomSanitizer } from '@angular/platform-browser'; +import { ConstructorService } from '../../service/constructor.service'; +import { LoadingService } from '../../service/loading.service'; +import { signal, WritableSignal, computed } from '@angular/core'; +import type { Constructor, CumulativePointsData, DriverData, TrackData } from '@f123dashboard/shared'; + +describe('DashboardComponent', () => { + let component: DashboardComponent; + let fixture: ComponentFixture; + let mockDbDataService: Partial; + let allDriversSignal: WritableSignal; + let tracksSignal: WritableSignal; + let constructorsSignal: WritableSignal; + let cumulativePointsSignal: WritableSignal; + let mockTwitchApiService: jasmine.SpyObj; + let mockDomSanitizer: jasmine.SpyObj; + let mockConstructorService: jasmine.SpyObj; + let mockLoadingService: LoadingService; + + const mockDriverData: DriverData[] = [ + { + driver_id: 1, + driver_username: 'driver1', + driver_name: 'John', + driver_surname: 'Doe', + driver_description: 'Test driver', + driver_license_pt: 3, + driver_consistency_pt: 5, + driver_fast_lap_pt: 3, + drivers_dangerous_pt: 2, + driver_ingenuity_pt: 4, + driver_strategy_pt: 6, + driver_color: '#FF0000', + car_name: 'Car1', + car_overall_score: 80, + total_sprint_points: 5, + total_free_practice_points: 10, + total_qualifying_points: 20, + total_full_race_points: 0, + total_race_points: 40, + total_points: 75 + }, + { + driver_id: 2, + driver_username: 'driver2', + driver_name: 'Jane', + driver_surname: 'Smith', + driver_description: 'Test driver 2', + driver_license_pt: 3, + driver_consistency_pt: 4, + driver_fast_lap_pt: 2, + drivers_dangerous_pt: 1, + driver_ingenuity_pt: 3, + driver_strategy_pt: 5, + driver_color: '#0000FF', + car_name: 'Car2', + car_overall_score: 75, + total_sprint_points: 4, + total_free_practice_points: 8, + total_qualifying_points: 15, + total_full_race_points: 0, + total_race_points: 33, + total_points: 60 + } + ]; + + const mockTrackData: TrackData[] = [ + { + track_id: 1, + date: new Date('2026-04-10').toISOString(), + name: 'Monaco GP', + country: 'Monaco', + besttime_driver_time: '1:12.909', + username: 'driver1', + has_sprint: 0, + has_x2: 0 + }, + { + track_id: 2, + date: new Date('2026-04-20').toISOString(), + name: 'Imola GP', + country: 'Italy', + besttime_driver_time: '1:15.484', + username: 'driver2', + has_sprint: 1, + has_x2: 0 + }, + { + track_id: 3, + date: new Date('2025-12-01').toISOString(), + name: 'Abu Dhabi GP', + country: 'UAE', + besttime_driver_time: '1:26.103', + username: 'driver1', + has_sprint: 0, + has_x2: 1 + } + ]; + + const mockConstructorData: Constructor[] = [ + { + constructor_id: 1, + constructor_name: 'Team Red', + constructor_color: '#FF0000', + driver_1_id: 1, + driver_1_username: 'driver1', + driver_1_tot_points: 75, + driver_2_id: 2, + driver_2_username: 'driver2', + driver_2_tot_points: 60, + constructor_tot_points: 135, + constructor_gained_points: 0 + } + ]; + + const mockCumulativePointsData: CumulativePointsData[] = [ + { + driver_id: 1, + driver_username: 'driver1', + driver_color: '#FF0000', + date: new Date('2026-01-30').toISOString(), + track_name: 'Track 4', + cumulative_points: 75 + }, + { + driver_id: 2, + driver_username: 'driver2', + driver_color: '#0000FF', + date: new Date('2026-01-30').toISOString(), + track_name: 'Track 4', + cumulative_points: 60 + }, + { + driver_id: 1, + driver_username: 'driver1', + driver_color: '#FF0000', + date: new Date('2026-01-20').toISOString(), + track_name: 'Track 3', + cumulative_points: 60 + }, + { + driver_id: 2, + driver_username: 'driver2', + driver_color: '#0000FF', + date: new Date('2026-01-20').toISOString(), + track_name: 'Track 3', + cumulative_points: 48 + }, + { + driver_id: 1, + driver_username: 'driver1', + driver_color: '#FF0000', + date: new Date('2026-01-10').toISOString(), + track_name: 'Track 2', + cumulative_points: 45 + }, + { + driver_id: 2, + driver_username: 'driver2', + driver_color: '#0000FF', + date: new Date('2026-01-10').toISOString(), + track_name: 'Track 2', + cumulative_points: 35 + }, + { + driver_id: 1, + driver_username: 'driver1', + driver_color: '#FF0000', + date: new Date('2026-01-01').toISOString(), + track_name: 'Track 1', + cumulative_points: 30 + }, + { + driver_id: 2, + driver_username: 'driver2', + driver_color: '#0000FF', + date: new Date('2026-01-01').toISOString(), + track_name: 'Track 1', + cumulative_points: 22 + } + ]; + + beforeEach(async () => { + // Create a writable signal for testing that can be updated + const mockIsLiveSignal = signal(false); + + allDriversSignal = signal(mockDriverData); + tracksSignal = signal(mockTrackData); + constructorsSignal = signal(mockConstructorData); + cumulativePointsSignal = signal(mockCumulativePointsData); + mockDbDataService = { + allDrivers: allDriversSignal.asReadonly(), + tracks: tracksSignal.asReadonly(), + constructors: computed(() => constructorsSignal()), + cumulativePoints: cumulativePointsSignal.asReadonly() + }; + + mockTwitchApiService = jasmine.createSpyObj('TwitchApiService', [ + 'getChannel', + 'checkStreamStatus' + ]); + // Assign the writable signal to the mock (casting as any to allow assignment in tests) + (mockTwitchApiService as any).isLive = mockIsLiveSignal.asReadonly(); + (mockTwitchApiService as any)._isLiveSignal = mockIsLiveSignal; + + mockDomSanitizer = jasmine.createSpyObj('DomSanitizer', [ + 'bypassSecurityTrustResourceUrl' + ]); + mockConstructorService = jasmine.createSpyObj('ConstructorService', [ + 'calculateConstructorPoints', + 'calculateConstructorGainedPoints' + ]); + mockLoadingService = { + isLoading: signal(false), + show: jasmine.createSpy('show'), + hide: jasmine.createSpy('hide') + } as any; + + mockTwitchApiService.getChannel.and.returnValue('testchannel'); + mockDomSanitizer.bypassSecurityTrustResourceUrl.and.returnValue('safe-url' as any); + mockConstructorService.calculateConstructorPoints.and.returnValue(mockConstructorData); + mockConstructorService.calculateConstructorGainedPoints.and.returnValue(mockConstructorData); + + await TestBed.configureTestingModule({ + imports: [DashboardComponent], + providers: [provideNoopAnimations(), { provide: DbDataService, useValue: mockDbDataService as DbDataService }, + { provide: TwitchApiService, useValue: mockTwitchApiService }, + { provide: DomSanitizer, useValue: mockDomSanitizer }, + { provide: ConstructorService, useValue: mockConstructorService }, + { provide: LoadingService, useValue: mockLoadingService } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(DashboardComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('ngOnInit', () => { + it('should initialize all data on component initialization', () => { + component.ngOnInit(); + + expect(component.championship_standings_users().length).toBe(2); + expect(component.championshipTracks().length).toBe(3); + }); + + it('should initialize Twitch embed when stream is live', () => { + (mockTwitchApiService as any)._isLiveSignal.set(true); + + component.ngOnInit(); + + expect(component.isLive()).toBe(true); + expect(mockDomSanitizer.bypassSecurityTrustResourceUrl).toHaveBeenCalled(); + }); + + it('should not initialize Twitch embed when stream is not live', () => { + (mockTwitchApiService as any)._isLiveSignal.set(false); + + component.ngOnInit(); + + expect(component.isLive()).toBe(false); + }); + + it('should initialize drivers with gainedPoints calculated from cumulative data', () => { + component.ngOnInit(); + + const drivers = component.championship_standings_users(); + // GainedPoints calculated as: last race (Track 4) - 3rd to last race (Track 2) + expect(drivers[0].gainedPoints).toBe(30); // 75 - 45 + expect(drivers[1].gainedPoints).toBe(25); // 60 - 35 + }); + + it('should calculate next tracks correctly', () => { + component.ngOnInit(); + + const nextTracks = component.championshipNextTracks(); + expect(nextTracks.length).toBe(2); + expect(nextTracks[0].name).toBe('Monaco GP'); + expect(nextTracks[1].name).toBe('Imola GP'); + }); + + it('should map tracks to calendar events', () => { + component.ngOnInit(); + + const events = component.calendarEvents(); + expect(events.length).toBe(3); + expect(events[0].name).toBe('Monaco GP'); + expect(events[0].description).toContain('Paese: Monaco'); + }); + + it('should add sprint indicator to calendar event description', () => { + component.ngOnInit(); + + const events = component.calendarEvents(); + const imolaEvent = events.find(e => e.name === 'Imola GP'); + expect(imolaEvent?.description).toContain('Sprint Race'); + }); + + it('should add double points indicator to calendar event description', () => { + component.ngOnInit(); + + const events = component.calendarEvents(); + const abuDhabiEvent = events.find(e => e.name === 'Abu Dhabi GP'); + expect(abuDhabiEvent?.description).toContain('Punti Doppi (x2)'); + }); + + it('should calculate constructor points', () => { + component.ngOnInit(); + + expect(mockConstructorService.calculateConstructorPoints).toHaveBeenCalledWith( + mockConstructorData, + jasmine.any(Array) + ); + expect(component.constructors().length).toBe(1); + }); + + it('should calculate driver gained points when sufficient data exists', () => { + component.ngOnInit(); + + const drivers = component.championship_standings_users(); + expect(drivers[0].gainedPoints).toBe(30); // 75 - 45 + expect(drivers[1].gainedPoints).toBe(25); // 60 - 35 + expect(component.showGainedPointsColumn()).toBe(true); + }); + + it('should calculate delta points between consecutive drivers', () => { + component.ngOnInit(); + + const drivers = component.championship_standings_users(); + expect(drivers[1].deltaPoints).toBe(15); // 75 - 60 + }); + + it('should calculate driver of the week', () => { + component.ngOnInit(); + + const driverOfWeek = component.driverOfWeek(); + // Calculates from last race (Track 4) to 3rd last race (Track 2) + // driver1: 75 - 45 = 30 points gained in last 2 races + expect(driverOfWeek.driver_username).toBe('driver1'); + expect(driverOfWeek.points).toBe(30); + }); + + it('should calculate constructor of the week', () => { + component.ngOnInit(); + + const constructorOfWeek = component.constructorOfWeek(); + // Team Red has driver1 (30 points) + driver2 (25 points) = 55 points in last 2 races + expect(constructorOfWeek.constructor_name).toBe('Team Red'); + expect(constructorOfWeek.points).toBe(55); + }); + }); + + describe('Pilot Modal', () => { + it('should open pilot modal with correct data', () => { + const pilot = mockDriverData[0]; + const position = 1; + + component.openPilotModal(pilot, position); + + expect(component.pilotModalVisible()).toBe(true); + expect(component.selectedPilot()).toEqual(pilot); + expect(component.selectedPilotPosition()).toBe(position); + }); + + it('should close pilot modal and clear data', () => { + component.openPilotModal(mockDriverData[0], 1); + component.closePilotModal(); + + expect(component.pilotModalVisible()).toBe(false); + expect(component.selectedPilot()).toBeNull(); + expect(component.selectedPilotPosition()).toBe(0); + }); + }); + + describe('Constructor Modal', () => { + it('should open constructor modal with correct data', () => { + const constructor = mockConstructorData[0]; + const position = 1; + + component.openConstructorModal(constructor, position); + + expect(component.constructorModalVisible()).toBe(true); + expect(component.selectedConstructor()).toEqual(constructor); + expect(component.selectedConstructorPosition()).toBe(position); + }); + + it('should close constructor modal and clear data', () => { + component.openConstructorModal(mockConstructorData[0], 1); + component.closeConstructorModal(); + + expect(component.constructorModalVisible()).toBe(false); + expect(component.selectedConstructor()).toBeNull(); + expect(component.selectedConstructorPosition()).toBe(0); + }); + }); + + describe('Result Modal', () => { + it('should toggle result modal visibility', () => { + component.toggleResoultModalvisible(1); + expect(component.resoultModalVisible()).toBe(1); + + component.toggleResoultModalvisible(2); + expect(component.resoultModalVisible()).toBe(2); + + component.toggleResoultModalvisible(0); + expect(component.resoultModalVisible()).toBe(0); + }); + }); + + describe('getConstructorForDriver', () => { + beforeEach(() => { + component.ngOnInit(); + }); + + it('should return constructor name for driver1', () => { + const constructorName = component.getConstructorForDriver('driver1'); + expect(constructorName).toBe('Team Red'); + }); + + it('should return constructor name for driver2', () => { + const constructorName = component.getConstructorForDriver('driver2'); + expect(constructorName).toBe('Team Red'); + }); + + it('should return empty string for unknown driver', () => { + const constructorName = component.getConstructorForDriver('unknown'); + expect(constructorName).toBe(''); + }); + }); + + describe('showColumn computed signal', () => { + it('should return true when screen width is greater than 1600', () => { + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: 1920 + }); + + const newFixture = TestBed.createComponent(DashboardComponent); + const newComponent = newFixture.componentInstance; + expect(newComponent.showColumn()).toBe(true); + }); + + it('should return false when screen width is less than or equal to 1600', () => { + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: 1400 + }); + + const newFixture = TestBed.createComponent(DashboardComponent); + const newComponent = newFixture.componentInstance; + expect(newComponent.showColumn()).toBe(false); + }); + }); + + describe('Edge cases', () => { + it('should handle empty driver data', () => { + allDriversSignal.set([]); + cumulativePointsSignal.set([]); + + component.ngOnInit(); + + expect(component.championship_standings_users().length).toBe(0); + expect(component.showGainedPointsColumn()).toBe(false); + }); + + it('should handle empty track data', () => { + tracksSignal.set([]); + + component.ngOnInit(); + + expect(component.championshipTracks().length).toBe(0); + expect(component.championshipNextTracks().length).toBe(0); + expect(component.calendarEvents().length).toBe(0); + }); + + it('should handle insufficient cumulative data for gained points calculation', () => { + const limitedCumulativeData: CumulativePointsData[] = [ + { + driver_id: 1, + driver_username: 'driver1', + driver_color: '#FF0000', + date: new Date('2026-01-01').toISOString(), + track_name: 'Track 1', + cumulative_points: 30 + } + ]; + cumulativePointsSignal.set(limitedCumulativeData); + + component.ngOnInit(); + + const drivers = component.championship_standings_users(); + drivers.forEach(driver => { + expect(driver.gainedPoints).toBe(0); + }); + expect(component.showGainedPointsColumn()).toBe(false); + }); + + it('should calculate constructor of the week when driver IDs come as strings from PostgreSQL (int8)', () => { + // PostgreSQL int8 columns are returned as strings by the pg driver at runtime + const constructorWithStringIds = [{ + ...mockConstructorData[0], + driver_1_id: '1' as unknown as number, + driver_2_id: '2' as unknown as number, + }]; + mockConstructorService.calculateConstructorPoints.and.returnValue(constructorWithStringIds); + mockConstructorService.calculateConstructorGainedPoints.and.returnValue(constructorWithStringIds); + + component.ngOnInit(); + + const constructorOfWeek = component.constructorOfWeek(); + // driver1 gained 30pts (75-45) + driver2 gained 25pts (60-35) = 55 total + expect(constructorOfWeek.constructor_name).toBe('Team Red'); + expect(constructorOfWeek.points).toBe(55); + }); + + it('should handle constructor with no drivers', () => { + const emptyConstructor: Constructor[] = [ + { + constructor_id: 1, + constructor_name: 'Empty Team', + constructor_color: '#000000', + driver_1_id: 0, + driver_1_username: '', + driver_1_tot_points: 0, + driver_2_id: 0, + driver_2_username: '', + driver_2_tot_points: 0, + constructor_tot_points: 0, + constructor_gained_points: 0 + } + ]; + constructorsSignal.set(emptyConstructor); + mockConstructorService.calculateConstructorPoints.and.returnValue(emptyConstructor); + mockConstructorService.calculateConstructorGainedPoints.and.returnValue(emptyConstructor); + + component.ngOnInit(); + + const constructorName = component.getConstructorForDriver('driver1'); + expect(constructorName).toBe(''); + }); + }); +}); diff --git a/client/src/app/views/dashboard/dashboard.component.ts b/client/src/app/views/dashboard/dashboard.component.ts new file mode 100644 index 000000000..0ce27a0ce --- /dev/null +++ b/client/src/app/views/dashboard/dashboard.component.ts @@ -0,0 +1,419 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnInit, inject, signal, computed, ChangeDetectionStrategy } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; +import {DbDataService} from '../../service/db-data.service'; +import { ConstructorService } from '../../service/constructor.service'; +import { ModalModule } from '@coreui/angular'; +import { + AvatarComponent, + CardBodyComponent, + CardComponent, + CardHeaderComponent, + ColComponent, + RowComponent, + TableDirective, + TextColorDirective, + BadgeComponent, + CarouselComponent, + CarouselControlComponent, + CarouselIndicatorsComponent, + CarouselInnerComponent, + CarouselItemComponent, + ThemeDirective, + Tabs2Module, + ButtonCloseDirective +} from '@coreui/angular'; +import { RouterLink } from '@angular/router'; +import { IconDirective } from '@coreui/icons-angular'; +import { cilCalendar, cilMap, cilFire } from '@coreui/icons'; +import { LeaderboardComponent } from "../../components/leaderboard/leaderboard.component"; +import { TwitchApiService } from '../../service/twitch-api.service'; +import { LoadingService } from '../../service/loading.service'; +import { ChampionshipTrendComponent } from '../../components/championship-trend/championship-trend.component'; +import type { Constructor, CumulativePointsData, DriverData, TrackData } from '@f123dashboard/shared'; +import { PilotCardComponent } from '../../components/pilot-card/pilot-card.component'; +import { ConstructorCardComponent } from '../../components/constructor-card/constructor-card.component'; +import { CalendarComponent, CalendarEvent } from '../../components/calendar/calendar.component'; +import type { DriverOfWeek, ConstructorOfWeek } from '../../model/dashboard.model'; +import { allFlags } from '../../model/constants'; + +export interface DriverDataWithGainedPoints extends DriverData { + gainedPoints: number; + deltaPoints?: number; +} + +@Component({ + selector: 'app-dashboard', + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + LeaderboardComponent, + ModalModule, + BadgeComponent, + ThemeDirective, + CarouselComponent, + CarouselIndicatorsComponent, + CarouselInnerComponent, + CarouselItemComponent, + CarouselControlComponent, + RouterLink, + CommonModule, + TextColorDirective, + CardComponent, + CardBodyComponent, + RowComponent, + ColComponent, + IconDirective, + ReactiveFormsModule, + CardHeaderComponent, + TableDirective, + AvatarComponent, + ChampionshipTrendComponent, + Tabs2Module, + PilotCardComponent, + ButtonCloseDirective, + ConstructorCardComponent, + CalendarComponent +] +}) +export class DashboardComponent implements OnInit { + private dbData = inject(DbDataService); + private twitchApiService = inject(TwitchApiService); + private sanitizer = inject(DomSanitizer); + private constructorService = inject(ConstructorService); + loadingService = inject(LoadingService); + + private screenWidth = signal(0); + showColumn = computed(() => this.screenWidth() > 1600); + + twitchEmbedUrl = signal('' as SafeResourceUrl); + calendarEvents = signal([]); + + championship_standings_users = signal([]); + championshipTracks = signal([]); + championshipNextTracks = signal([]); + isLive = signal(true); + constructors = signal([]); + showGainedPointsColumn = signal(false); + driverOfWeek = signal({ driver_username: '', driver_id: 0, points: 0 }); + constructorOfWeek = signal({ + constructor_name: '', + constructor_id: 0, + constructor_driver_1_id: 0, + constructor_driver_2_id: 0, + points: 0 + }); + + pilotModalVisible = signal(false); + selectedPilot = signal(null); + selectedPilotPosition = signal(0); + + constructorModalVisible = signal(false); + selectedConstructor = signal(null); + selectedConstructorPosition = signal(0); + + resoultModalVisible = signal(0); + + private constructorMap = computed(() => { + const map = new Map(); + this.constructors().forEach(c => { + if (c.driver_1_username) map.set(c.driver_1_username, c.constructor_name); + if (c.driver_2_username) map.set(c.driver_2_username, c.constructor_name); + }); + return map; + }); + + readonly allFlags = allFlags; + public calendarIcon: string[] = cilCalendar; + public mapIcon: string[] = cilMap; + public fireIcon: string[] = cilFire; + + constructor() { + this.screenWidth.set(window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth); + } + + toggleResoultModalvisible(modal: number): void { + this.resoultModalVisible.set(modal); + } + + ngOnInit(): void { + this.initializeTwitchEmbed(); + this.initializeDriversData(); + this.initializeTracksAndCalendar(); + this.initializeConstructorsData(); + this.calculateNextTracks(); + + const championshipTrend = this.getChampionshipTrend(); + this.calculateDriverGainedPoints(championshipTrend); + this.calculateConstructorGainedPoints(); + this.calculateDriverDeltaPoints(); + this.calculateWeeklyBestPerformers(championshipTrend); + } + + /** + * Initializes Twitch embed URL if stream is live + */ + private initializeTwitchEmbed(): void { + this.isLive.set(this.twitchApiService.isLive()); + if (this.isLive()) { + const currentHostname = window.location.hostname; + const twitchUrl = `https://player.twitch.tv/?channel=${this.twitchApiService.getChannel()}&parent=${currentHostname}&autoplay=false&muted=false&time=0s`; + this.twitchEmbedUrl.set(this.sanitizer.bypassSecurityTrustResourceUrl(twitchUrl)); + } + } + + /** + * Initializes drivers data with gained points initialized to 0 + */ + private initializeDriversData(): void { + this.championship_standings_users.set(this.dbData.allDrivers().map(driver => ({ + ...driver, + gainedPoints: 0 + }))); + } + + /** + * Gets championship trend data sorted by date + */ + private getChampionshipTrend(): CumulativePointsData[] { + return this.dbData.cumulativePoints() + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + } + + /** + * Initializes tracks data and maps them to calendar events + */ + private initializeTracksAndCalendar(): void { + this.championshipTracks.set(this.dbData.tracks()); + this.calendarEvents.set(this.mapTracksToCalendarEvents(this.championshipTracks())); + } + + /** + * Maps track data to calendar event format + */ + private mapTracksToCalendarEvents(tracks: TrackData[]): CalendarEvent[] { + return tracks.map(track => { + let desc = `Paese: ${track.country}`; + if (track.has_sprint === 1) { + desc += '\n• Sprint Race'; + } + if (track.has_x2 === 1) { + desc += '\n• Punti Doppi (x2)'; + } + + return { + name: track.name, + date: track.date, + description: desc + }; + }); + } + + /** + * Initializes constructors data and calculates their points + */ + private initializeConstructorsData(): void { + const allConstructors = this.dbData.constructors(); + const updatedConstructors = this.constructorService.calculateConstructorPoints( + allConstructors, + this.championship_standings_users() + ); + this.constructors.set(updatedConstructors); + } + + /** + * Filters and formats the next 2 upcoming championship tracks + */ + private calculateNextTracks(): void { + const currentDate = new Date(); + currentDate.setHours(0, 0, 0, 0); + + const nextTracks = this.championshipTracks() + .map(track => { + const dbDate = new Date(track.date); + dbDate.setHours(0, 0, 0, 0); + return { ...track, date: dbDate, dbDate }; + }) + .filter(track => track.dbDate >= currentDate) + .slice(0, 2) + .map(track => ({ + ...track, + date: track.dbDate.toLocaleDateString("it-CH") + })); + + this.championshipNextTracks.set(nextTracks); + } + + /** + * Calculates gained points for drivers over the last 2 tracks + */ + private calculateDriverGainedPoints(championshipTrend: CumulativePointsData[]): void { + const users = this.championship_standings_users(); + let hasGainedPoints = false; + + for (const user of users) { + const userTracks = championshipTrend.filter( + (track: CumulativePointsData) => track.driver_username === user.driver_username + ); + + if (userTracks.length > 2) { + const lastPoints = userTracks[0].cumulative_points; + const thirdToLastPoints = userTracks[2].cumulative_points; + user.gainedPoints = lastPoints - thirdToLastPoints; + hasGainedPoints = true; + } else { + user.gainedPoints = 0; + } + } + + this.championship_standings_users.set([...users]); + this.showGainedPointsColumn.set(hasGainedPoints); + } + + /** + * Calculates gained points for constructors + */ + private calculateConstructorGainedPoints(): void { + const updatedConstructors = this.constructorService.calculateConstructorGainedPoints( + this.constructors(), + this.championship_standings_users() + ); + this.constructors.set(updatedConstructors); + } + + /** + * Calculates delta points between consecutive drivers in standings + */ + private calculateDriverDeltaPoints(): void { + const users = this.championship_standings_users(); + + for (let i = 1; i < users.length; i++) { + users[i].deltaPoints = users[i - 1].total_points - users[i].total_points; + } + + this.championship_standings_users.set([...users]); + } + + /** + * Calculates the best performing driver and constructor of the week + */ + private calculateWeeklyBestPerformers(championshipTrend: CumulativePointsData[]): void { + const driversCount = this.championship_standings_users().length; + + if (championshipTrend.length <= 3 * driversCount) { + return; + } + + const lastRacePoints = championshipTrend.slice(0, driversCount); + const thirdLastRacePoints = championshipTrend.slice(2 * driversCount, 3 * driversCount); + + this.calculateDriverOfWeek(lastRacePoints, thirdLastRacePoints); + this.calculateConstructorOfWeek(lastRacePoints, thirdLastRacePoints); + } + + /** + * Finds the driver with the most points gained in the last 2 races + */ + private calculateDriverOfWeek( + lastRacePoints: CumulativePointsData[], + thirdLastRacePoints: CumulativePointsData[] + ): void { + let bestPoints = 0; + let bestDriver: DriverOfWeek = { driver_username: '', driver_id: 0, points: 0 }; + + for (let i = 0; i < lastRacePoints.length; i++) { + const currentPoints = Number(lastRacePoints[i].cumulative_points) - + Number(thirdLastRacePoints[i].cumulative_points); + + if (currentPoints > bestPoints) { + bestPoints = currentPoints; + bestDriver = { + driver_username: lastRacePoints[i].driver_username, + driver_id: Number(lastRacePoints[i].driver_id), + points: currentPoints + }; + } + } + + this.driverOfWeek.set(bestDriver); + } + + /** + * Finds the constructor with the most combined driver points in the last 2 races + */ + private calculateConstructorOfWeek( + lastRacePoints: CumulativePointsData[], + thirdLastRacePoints: CumulativePointsData[] + ): void { + const constructorsOfWeek: ConstructorOfWeek[] = this.constructors().map(constructor => ({ + constructor_name: constructor.constructor_name, + constructor_id: constructor.constructor_id, + constructor_driver_1_id: Number(constructor.driver_1_id), + constructor_driver_2_id: Number(constructor.driver_2_id), + points: 0 + })); + + for (let i = 0; i < lastRacePoints.length; i++) { + const currentPoints = Number(lastRacePoints[i].cumulative_points) - + Number(thirdLastRacePoints[i].cumulative_points); + + constructorsOfWeek.forEach(constructor => { + const driverId = Number(lastRacePoints[i].driver_id); + if (constructor.constructor_driver_1_id === driverId || + constructor.constructor_driver_2_id === driverId) { + constructor.points += currentPoints; + } + }); + } + + const sortedConstructors = constructorsOfWeek.sort((a, b) => b.points - a.points); + if (sortedConstructors.length > 0) { + this.constructorOfWeek.set(sortedConstructors[0]); + } + } + + /** + * Opens the pilot modal with the selected pilot data + */ + openPilotModal(pilot: DriverData, position: number): void { + this.selectedPilot.set(pilot); + this.selectedPilotPosition.set(position); + this.pilotModalVisible.set(true); + } + + /** + * Closes the pilot modal + */ + closePilotModal(): void { + this.pilotModalVisible.set(false); + this.selectedPilot.set(null); + this.selectedPilotPosition.set(0); + } + + /** + * Opens the constructor modal with the selected constructor data + */ + openConstructorModal(constructor: Constructor, position: number): void { + this.selectedConstructor.set(constructor); + this.selectedConstructorPosition.set(position); + this.constructorModalVisible.set(true); + } + + /** + * Closes the constructor modal + */ + closeConstructorModal(): void { + this.constructorModalVisible.set(false); + this.selectedConstructor.set(null); + this.selectedConstructorPosition.set(0); + } + + /** + * Get constructor name for a driver by matching username + */ + getConstructorForDriver(driverUsername: string): string { + return this.constructorMap().get(driverUsername) || ''; + } +} \ No newline at end of file diff --git a/src/app/views/dashboard/routes.ts b/client/src/app/views/dashboard/routes.ts similarity index 100% rename from src/app/views/dashboard/routes.ts rename to client/src/app/views/dashboard/routes.ts diff --git a/client/src/app/views/fanta-dashboard/fanta-dashboard.component.html b/client/src/app/views/fanta-dashboard/fanta-dashboard.component.html new file mode 100644 index 000000000..432da4a8e --- /dev/null +++ b/client/src/app/views/fanta-dashboard/fanta-dashboard.component.html @@ -0,0 +1,11 @@ + + +

Fanta di RaceForFederica🎰

+

Competi con altri giocatori per stabile chi ne sa di più sui nostri eroi.
+ Ogni settimana, indovina le posizioni finali dei piloti nel Gran Premio e guadagna punti!

+ +

Classifica

+ +
+
+
\ No newline at end of file diff --git a/client/src/app/views/fanta-dashboard/fanta-dashboard.component.spec.ts b/client/src/app/views/fanta-dashboard/fanta-dashboard.component.spec.ts new file mode 100644 index 000000000..72266141c --- /dev/null +++ b/client/src/app/views/fanta-dashboard/fanta-dashboard.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; + +import { FantaDashboardComponent } from './fanta-dashboard.component'; + +describe('FantaDashboardComponent', () => { + let component: FantaDashboardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [provideNoopAnimations(), ], + imports: [FantaDashboardComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FantaDashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/views/fanta-dashboard/fanta-dashboard.component.ts b/client/src/app/views/fanta-dashboard/fanta-dashboard.component.ts new file mode 100644 index 000000000..0b8ea387d --- /dev/null +++ b/client/src/app/views/fanta-dashboard/fanta-dashboard.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { LeaderboardComponent } from "../../components/leaderboard/leaderboard.component"; +import { FantaRulesComponent } from '../../components/fanta-rules/fanta-rules.component'; +import { GridModule } from '@coreui/angular'; + +@Component({ + selector: 'app-fanta-dashboard', + imports: [ + GridModule, + LeaderboardComponent + ], + templateUrl: './fanta-dashboard.component.html' +}) +export class FantaDashboardComponent { + +} diff --git a/client/src/app/views/fanta-dashboard/routes.ts b/client/src/app/views/fanta-dashboard/routes.ts new file mode 100644 index 000000000..6d65fe04e --- /dev/null +++ b/client/src/app/views/fanta-dashboard/routes.ts @@ -0,0 +1,12 @@ +import { Routes } from '@angular/router'; + +export const routes: Routes = [ + { + path: '', + loadComponent: () => import('./fanta-dashboard.component').then(m => m.FantaDashboardComponent), + data: { + title: $localize`Fanta Dashboard` + } + } +]; + diff --git a/client/src/app/views/fanta/fanta.component.html b/client/src/app/views/fanta/fanta.component.html new file mode 100644 index 000000000..4f252dcd1 --- /dev/null +++ b/client/src/app/views/fanta/fanta.component.html @@ -0,0 +1,325 @@ +
+ + + + + + + + + + + +
+
+ +
+
+
+

{{ user().username }}

+
+
+
+
+ Punteggio: {{ userFantaPoints() }} +
+
+
+
+ +
+ + +
+
+
+
+ + +

Votazioni gare future

+

+ In questa sezione puoi inserire le votazione per le prossime 4 gare +

+
+ + + @for (track of nextTracks(); track track.track_id) { + + {{ track.name }} + + + + +
+
+ + @for (posizione of posizioni | keyvalue; track posizione.key) { + + + + @if (posizione.key <= 8) { + {{ posizione?.value }} posto + } @else { + {{posizione?.value}} + } + + @if (posizione.key <= 3) { + + } + @if (posizione.key == 9) { + + } + @if (posizione.key == 10) { + + } + @if (posizione.key == 11) { + + } + + + @if (posizione.key <= 10) { + + } @else { + + } + + + } + + + + + + + + @if (formStatus()[track.track_id] == 2) { +
+ @if (validationDetails()[track.track_id]?.hasEmptyVotes) { +

+ Errore: alcuni campi sono vuoti o non sono stati selezionati +

+ } + @if (validationDetails()[track.track_id]?.hasDuplicates) { +

+ Errore: hai selezionato lo stesso pilota più volte +

+ } +
+ } + @if (formStatus()[track.track_id] == 3) { +

+ Errore nel salvataggio: contattare l'assistenza :( +

+ } + @if (formStatus()[track.track_id] == 1) { +

+ Preferenze salvate correttamente :) +

+ } +
+ +
+
+
+
+
+
+ } +
+
+
+ @if (previusTracks().length > 0) { + + +
+

Storico Votazioni

+

+ In questa sezione puoi visualizzare i punti guadagnati +

+
+
+ + + + @for (track of previusTracks(); track track.track_id) { + + {{ track.name }} + + + + @if (votazioni().has(track.track_id)) { + +
+ + + + + + +
+
+ } +
+ } +
+
+
+ } +
+ + +
Classifica Fanta
+ +
+ + + +
+ + +
Regolamento Punteggi Fanta
+ +
+ + + +
\ No newline at end of file diff --git a/client/src/app/views/fanta/fanta.component.scss b/client/src/app/views/fanta/fanta.component.scss new file mode 100644 index 000000000..abcdc7c07 --- /dev/null +++ b/client/src/app/views/fanta/fanta.component.scss @@ -0,0 +1,175 @@ +.custom-table { + border-collapse: separate; + border-spacing: 0; + overflow: hidden; /* Nasconde i bordi sporgenti */ + padding: 8px; + } + + .titolo { + margin-top: 60px; + margin-bottom: 15px; + } + + .storico { + margin-bottom: 25px; + } + + .cTable { + background-color: transparent !important; + } + + th, td { + padding: 5px; + } + + .position-label { + display: flex; + align-items: center; + gap: 0.25rem; + + strong { + white-space: nowrap; + font-size: 0.95rem; + } + } + + .medal-icon { + width: 20px; + height: auto; + margin-left: 0.25rem; + } + + .position-icon { + width: 18px; + height: 18px; + margin-left: 0.25rem; + } + + .select-col { + select { + width: 100%; + font-size: 0.9rem; + } + } + + .position-row { + align-items: center; + } + + .submit-row { + margin-top: 2rem; + margin-bottom: 1rem; + } + + .status-row { + min-height: 40px; + } + + .confirm-button { + min-width: 150px; + padding: 0.5rem 2rem; + } + + // Large screen optimizations + @media (min-width: 768px) { + .position-row { + max-width: 700px; + margin-left: auto; + margin-right: auto; + } + + .position-label { + flex: 0 0 auto; + min-width: 150px; + } + + .select-col { + flex: 1 1 auto; + max-width: 400px; + } + + .submit-row, + .status-row { + max-width: 700px; + margin-left: auto; + margin-right: auto; + } + } + + @media (min-width: 992px) { + .position-row { + max-width: 800px; + } + + .position-label { + min-width: 180px; + } + + .select-col { + max-width: 500px; + } + + .submit-row, + .status-row { + max-width: 800px; + } + } + + @media (min-width: 1200px) { + .position-row { + max-width: 900px; + } + + .submit-row, + .status-row { + max-width: 900px; + } + } + + @media (max-width: 650px) { + .accordion-body{ + padding: var(--cui-accordion-body-padding-y) 0.5rem; + } + + .position-label { + padding-right: 0.5rem; + + strong { + font-size: 0.85rem; + } + } + + .medal-icon { + width: 16px; + } + + .position-icon { + width: 16px; + height: 16px; + } + + .select-col { + padding-left: 0.5rem; + + select { + font-size: 0.85rem; + padding: 0.375rem 0.5rem; + } + } + } + + @media (max-width: 400px) { + .position-label { + strong { + font-size: 0.8rem; + } + } + + .select-col { + select { + font-size: 0.8rem; + padding: 0.25rem 0.4rem; + } + } + } + \ No newline at end of file diff --git a/client/src/app/views/fanta/fanta.component.spec.ts b/client/src/app/views/fanta/fanta.component.spec.ts new file mode 100644 index 000000000..031a7dc6b --- /dev/null +++ b/client/src/app/views/fanta/fanta.component.spec.ts @@ -0,0 +1,623 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { signal, WritableSignal, computed } from '@angular/core'; +import { NgForm } from '@angular/forms'; +import { FantaComponent } from './fanta.component'; +import { AuthService } from './../../service/auth.service'; +import { DbDataService } from './../../service/db-data.service'; +import { FantaService } from './../../service/fanta.service'; +import { VOTE_INDEX, FORM_STATUS } from '../../model/fanta'; +import type { FantaVote, User, DriverData, TrackData, Constructor } from '@f123dashboard/shared'; + +describe('FantaComponent', () => { + let component: FantaComponent; + let fixture: ComponentFixture; + let mockAuthService: jasmine.SpyObj; + let mockDbDataService: Partial; + let mockFantaService: jasmine.SpyObj; + let allDriversSignal: WritableSignal; + let tracksSignal: WritableSignal; + let constructorsSignal: WritableSignal; + let usersSignal: WritableSignal; + + const mockUser: User = { + id: 1, + username: 'testuser', + name: 'Test', + surname: 'User', + isAdmin: false + }; + + const mockDrivers: DriverData[] = [ + { + driver_id: 1, + driver_username: 'driver1', + driver_name: 'Driver', + driver_surname: 'One', + driver_description: 'Test driver', + driver_license_pt: 3, + driver_consistency_pt: 5, + driver_fast_lap_pt: 3, + drivers_dangerous_pt: 2, + driver_ingenuity_pt: 4, + driver_strategy_pt: 6, + driver_color: '#FF0000', + car_name: 'Car 1', + car_overall_score: 100, + total_sprint_points: 10, + total_free_practice_points: 10, + total_qualifying_points: 20, + total_full_race_points: 15, + total_race_points: 30, + total_points: 85 + }, + { + driver_id: 2, + driver_username: 'driver2', + driver_name: 'Driver', + driver_surname: 'Two', + driver_description: 'Test driver 2', + driver_license_pt: 3, + driver_consistency_pt: 6, + driver_fast_lap_pt: 4, + drivers_dangerous_pt: 1, + driver_ingenuity_pt: 5, + driver_strategy_pt: 7, + driver_color: '#00FF00', + car_name: 'Car 2', + car_overall_score: 95, + total_sprint_points: 12, + total_free_practice_points: 15, + total_qualifying_points: 25, + total_full_race_points: 18, + total_race_points: 35, + total_points: 105 + } + ]; + + const mockTracks: TrackData[] = [ + { + track_id: 1, + name: 'Monaco', + date: '2026-05-15', + country: 'Monaco', + has_sprint: 0, + has_x2: 0, + besttime_driver_time: '1:23.456', + username: 'driver1' + }, + { + track_id: 2, + name: 'Silverstone', + date: '2026-07-10', + country: 'United Kingdom', + has_sprint: 1, + has_x2: 0, + besttime_driver_time: '1:24.567', + username: 'driver2' + }, + { + track_id: 3, + name: 'Monza', + date: '2026-01-10', + country: 'Italy', + has_sprint: 0, + has_x2: 0, + besttime_driver_time: '1:22.345', + username: 'driver1' + } + ]; + + const mockConstructors: Constructor[] = [ + { + constructor_id: 1, + constructor_name: 'Red Bull Racing', + constructor_color: '#0600EF', + driver_1_id: 1, + driver_1_username: 'driver1', + driver_1_tot_points: 100, + driver_2_id: 2, + driver_2_username: 'driver2', + driver_2_tot_points: 90, + constructor_tot_points: 190 + }, + { + constructor_id: 2, + constructor_name: 'Ferrari', + constructor_color: '#DC0000', + driver_1_id: 3, + driver_1_username: 'driver3', + driver_1_tot_points: 85, + driver_2_id: 4, + driver_2_username: 'driver4', + driver_2_tot_points: 80, + constructor_tot_points: 165 + } + ]; + + const mockFantaVote: FantaVote = { + fanta_player_id: 1, + username: 'testuser', + track_id: 1, + id_1_place: 1, + id_2_place: 2, + id_3_place: 3, + id_4_place: 4, + id_5_place: 5, + id_6_place: 6, + id_7_place: 7, + id_8_place: 8, + id_fast_lap: 1, + id_dnf: 2, + constructor_id: 1 + }; + + beforeEach(async () => { + // Mock services + mockAuthService = jasmine.createSpyObj('AuthService', [], { currentUser: signal(mockUser) }); + + allDriversSignal = signal(mockDrivers); + tracksSignal = signal(mockTracks); + constructorsSignal = signal(mockConstructors); + usersSignal = signal([mockUser]); + mockDbDataService = { + allDrivers: allDriversSignal.asReadonly(), + tracks: tracksSignal.asReadonly(), + constructors: computed(() => constructorsSignal()), + users: usersSignal.asReadonly(), + getAvatarSrc: jasmine.createSpy('getAvatarSrc').and.returnValue('assets/images/default-avatar.png') + }; + mockFantaService = jasmine.createSpyObj('FantaService', [ + 'getFantaVote', + 'setFantaVote', + 'getFantaPoints', + 'getFantaNumberVotes', // For LeaderboardComponent (per user) + 'getRaceResult', // For VoteHistoryTableComponent + 'isDnfCorrect', // For VoteHistoryTableComponent + 'pointsWithAbsoluteDifference', // For VoteHistoryTableComponent + 'getWinningConstructorsForTrack', // For VoteHistoryTableComponent + 'getFantaRacePoints' // For VoteHistoryTableComponent + ]); + + // Setup default spy returns + (mockDbDataService.getAvatarSrc as jasmine.Spy).and.returnValue('assets/images/default-avatar.png'); + mockFantaService.getFantaVote.and.returnValue(undefined); + mockFantaService.setFantaVote.and.returnValue(Promise.resolve()); + mockFantaService.getFantaPoints.and.returnValue(100); + (mockFantaService as any).totNumberVotes = signal(10); // For LeaderboardComponent + mockFantaService.getFantaNumberVotes.and.returnValue(5); // For LeaderboardComponent (per user) + mockFantaService.getRaceResult.and.returnValue(undefined); // For VoteHistoryTableComponent + mockFantaService.isDnfCorrect.and.returnValue(false); // For VoteHistoryTableComponent + mockFantaService.pointsWithAbsoluteDifference.and.returnValue(0); // For VoteHistoryTableComponent + mockFantaService.getWinningConstructorsForTrack.and.returnValue([]); // For VoteHistoryTableComponent + mockFantaService.getFantaRacePoints.and.returnValue(0); // For VoteHistoryTableComponent + + // Setup sessionStorage + sessionStorage.setItem('user', JSON.stringify(mockUser)); + + await TestBed.configureTestingModule({ + providers: [ + provideNoopAnimations(), + { provide: AuthService, useValue: mockAuthService }, + { provide: DbDataService, useValue: mockDbDataService as DbDataService }, + { provide: FantaService, useValue: mockFantaService } + ], + imports: [FantaComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(FantaComponent); + component = fixture.componentInstance; + }); + + afterEach(() => { + sessionStorage.clear(); + }); + + describe('Component Creation', () => { + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize with correct dependencies', () => { + expect(component.authService).toBeTruthy(); + expect(component.fantaService).toBeTruthy(); + }); + }); + + describe('ngOnInit', () => { + it('should load user from session storage', () => { + fixture.detectChanges(); + expect(component.user()).toEqual(mockUser); + }); + + it('should load drivers, tracks, and constructors', () => { + fixture.detectChanges(); + expect(component.piloti()).toEqual(mockDrivers); + expect(component.tracks()).toEqual(mockTracks); + }); + + it('should filter tracks into next and previous', () => { + fixture.detectChanges(); + expect(component.nextTracks().length).toBe(2); // Monaco and Silverstone are in the future + expect(component.previusTracks().length).toBe(1); // Monza is in the past + }); + + it('should load previous votes for all tracks', () => { + mockFantaService.getFantaVote.and.returnValue(mockFantaVote); + fixture.detectChanges(); + expect(mockFantaService.getFantaVote).toHaveBeenCalled(); + }); + }); + + describe('Vote Management', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should set vote correctly', () => { + const mockForm = { valid: true } as NgForm; + component.setVoto(1, 1, 5, mockForm); + expect(component.getVoto(1, 1)).toBe(5); + }); + + it('should get vote correctly', () => { + const mockForm = { valid: true } as NgForm; + component.setVoto(1, 1, 5, mockForm); + expect(component.getVoto(1, 1)).toBe(5); + }); + + it('should return 0 for non-existent vote', () => { + expect(component.getVoto(999, 1)).toBe(0); + }); + + it('should get all votes for a track', () => { + const mockForm = { valid: true } as NgForm; + component.setVoto(1, 1, 5, mockForm); + component.setVoto(1, 2, 6, mockForm); + const votes = component.getVoti(1); + expect(votes).toBeDefined(); + expect(votes![0]).toBe(5); + expect(votes![1]).toBe(6); + }); + + it('should get vote position for a driver', () => { + const mockForm = { valid: true } as NgForm; + component.setVoto(1, 1, 5, mockForm); + component.setVoto(1, 2, 6, mockForm); + expect(component.getVotoPos(1, 5)).toBe(1); + expect(component.getVotoPos(1, 6)).toBe(2); + }); + + it('should return 0 for driver not in vote list', () => { + expect(component.getVotoPos(1, 999)).toBe(0); + }); + + it('should reset form status when vote is changed after success', () => { + const mockForm = { valid: true } as NgForm; + component['formStatusSignal'].set({ 1: FORM_STATUS.SUCCESS }); + component.setVoto(1, 1, 5, mockForm); + expect(component.formStatus()[1]).toBeUndefined(); + }); + }); + + describe('Form Validation', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should return false for empty form', () => { + expect(component.formIsValid(1)).toBe(false); + }); + + it('should return false for incomplete form', () => { + const mockForm = { valid: true } as NgForm; + component.setVoto(1, 1, 1, mockForm); + component.setVoto(1, 2, 2, mockForm); + expect(component.formIsValid(1)).toBe(false); + }); + + it('should return false for duplicate driver votes', () => { + const mockForm = { valid: true } as NgForm; + // Set all required fields + component.setVoto(1, 1, 1, mockForm); + component.setVoto(1, 2, 1, mockForm); // Duplicate driver ID + component.setVoto(1, 3, 3, mockForm); + component.setVoto(1, 4, 4, mockForm); + component.setVoto(1, 5, 5, mockForm); + component.setVoto(1, 6, 6, mockForm); + component.setVoto(1, 7, 7, mockForm); + component.setVoto(1, 8, 8, mockForm); + component.setVoto(1, 9, 1, mockForm); // Fast lap + component.setVoto(1, 10, 2, mockForm); // DNF + component.setVoto(1, 11, 1, mockForm); // Constructor + expect(component.formIsValid(1)).toBe(false); + }); + + it('should return true for complete and valid form', () => { + const mockForm = { valid: true } as NgForm; + // Set all required fields with unique driver IDs + component.setVoto(1, 1, 1, mockForm); + component.setVoto(1, 2, 2, mockForm); + component.setVoto(1, 3, 3, mockForm); + component.setVoto(1, 4, 4, mockForm); + component.setVoto(1, 5, 5, mockForm); + component.setVoto(1, 6, 6, mockForm); + component.setVoto(1, 7, 7, mockForm); + component.setVoto(1, 8, 8, mockForm); + component.setVoto(1, 9, 1, mockForm); // Fast lap (can be duplicate) + component.setVoto(1, 10, 2, mockForm); // DNF (can be duplicate) + component.setVoto(1, 11, 1, mockForm); // Constructor + expect(component.formIsValid(1)).toBe(true); + }); + }); + + describe('Publishing Votes', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should not publish invalid form', async () => { + const mockForm = { valid: false } as NgForm; + await component.publishVoto(1, mockForm); + expect(mockFantaService.setFantaVote).not.toHaveBeenCalled(); + expect(component.formStatus()[1]).toBe(FORM_STATUS.VALIDATION_ERROR); + }); + + it('should publish valid vote successfully', async () => { + const mockForm = { valid: true } as NgForm; + // Set up a complete valid vote + component.setVoto(1, 1, 1, mockForm); + component.setVoto(1, 2, 2, mockForm); + component.setVoto(1, 3, 3, mockForm); + component.setVoto(1, 4, 4, mockForm); + component.setVoto(1, 5, 5, mockForm); + component.setVoto(1, 6, 6, mockForm); + component.setVoto(1, 7, 7, mockForm); + component.setVoto(1, 8, 8, mockForm); + component.setVoto(1, 9, 1, mockForm); + component.setVoto(1, 10, 2, mockForm); + component.setVoto(1, 11, 1, mockForm); + + await component.publishVoto(1, mockForm); + expect(mockFantaService.setFantaVote).toHaveBeenCalled(); + expect(component.formStatus()[1]).toBe(FORM_STATUS.SUCCESS); + }); + + it('should set loading state during publish', async () => { + const mockForm = { valid: true } as NgForm; + component.setVoto(1, 1, 1, mockForm); + component.setVoto(1, 2, 2, mockForm); + component.setVoto(1, 3, 3, mockForm); + component.setVoto(1, 4, 4, mockForm); + component.setVoto(1, 5, 5, mockForm); + component.setVoto(1, 6, 6, mockForm); + component.setVoto(1, 7, 7, mockForm); + component.setVoto(1, 8, 8, mockForm); + component.setVoto(1, 9, 1, mockForm); + component.setVoto(1, 10, 2, mockForm); + component.setVoto(1, 11, 1, mockForm); + + let loadingDuringCall = false; + mockFantaService.setFantaVote.and.callFake(async () => { + loadingDuringCall = component.isLoading(1); + return Promise.resolve(); + }); + + await component.publishVoto(1, mockForm); + expect(loadingDuringCall).toBe(true); + expect(component.isLoading(1)).toBe(false); + }); + + it('should handle save errors', async () => { + const mockForm = { valid: true } as NgForm; + component.setVoto(1, 1, 1, mockForm); + component.setVoto(1, 2, 2, mockForm); + component.setVoto(1, 3, 3, mockForm); + component.setVoto(1, 4, 4, mockForm); + component.setVoto(1, 5, 5, mockForm); + component.setVoto(1, 6, 6, mockForm); + component.setVoto(1, 7, 7, mockForm); + component.setVoto(1, 8, 8, mockForm); + component.setVoto(1, 9, 1, mockForm); + component.setVoto(1, 10, 2, mockForm); + component.setVoto(1, 11, 1, mockForm); + + mockFantaService.setFantaVote.and.returnValue(Promise.reject('Error')); + await component.publishVoto(1, mockForm); + expect(component.formStatus()[1]).toBe(FORM_STATUS.SAVE_ERROR); + expect(component.isLoading(1)).toBe(false); + }); + + it('should update original votes after successful save', async () => { + const mockForm = { valid: true } as NgForm; + component.setVoto(1, 1, 1, mockForm); + component.setVoto(1, 2, 2, mockForm); + component.setVoto(1, 3, 3, mockForm); + component.setVoto(1, 4, 4, mockForm); + component.setVoto(1, 5, 5, mockForm); + component.setVoto(1, 6, 6, mockForm); + component.setVoto(1, 7, 7, mockForm); + component.setVoto(1, 8, 8, mockForm); + component.setVoto(1, 9, 1, mockForm); + component.setVoto(1, 10, 2, mockForm); + component.setVoto(1, 11, 1, mockForm); + + await component.publishVoto(1, mockForm); + const originalVotes = component.originalVotazioni().get(1); + const currentVotes = component.votazioni().get(1); + expect(originalVotes).toEqual(currentVotes); + }); + }); + + describe('Change Detection', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should detect unsaved data when votes are changed', () => { + const mockForm = { valid: true } as NgForm; + component.setVoto(1, 1, 5, mockForm); + expect(component.hasUnsavedData(1)).toBe(true); + }); + + it('should not show unsaved data for empty form', () => { + expect(component.hasUnsavedData(1)).toBe(false); + }); + + it('should not show unsaved data after successful save', () => { + const mockForm = { valid: true } as NgForm; + component.setVoto(1, 1, 5, mockForm); + component['formStatusSignal'].set({ 1: FORM_STATUS.SUCCESS }); + expect(component.hasUnsavedData(1)).toBe(false); + }); + + it('should show no data when no votes exist', () => { + expect(component.hasNoData(1)).toBe(true); + }); + + it('should not show no data when votes exist', () => { + const mockForm = { valid: true } as NgForm; + component.setVoto(1, 1, 5, mockForm); + expect(component.hasNoData(1)).toBe(false); + }); + + it('should not show no data after successful save', () => { + component['formStatusSignal'].set({ 1: FORM_STATUS.SUCCESS }); + expect(component.hasNoData(1)).toBe(false); + }); + }); + + describe('Modal Management', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should toggle ranking modal', () => { + expect(component.modalRankingVisible()).toBe(false); + component.toggleModalRanking(); + expect(component.modalRankingVisible()).toBe(true); + component.toggleModalRanking(); + expect(component.modalRankingVisible()).toBe(false); + }); + + it('should toggle rules modal', () => { + expect(component.modalRulesVisible()).toBe(false); + component.toggleModalRules(); + expect(component.modalRulesVisible()).toBe(true); + component.toggleModalRules(); + expect(component.modalRulesVisible()).toBe(false); + }); + }); + + describe('Computed Properties', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should compute user fanta points', () => { + expect(component.userFantaPoints()).toBe(100); + expect(mockFantaService.getFantaPoints).toHaveBeenCalledWith(mockUser.id); + }); + + it('should throw error when user id is missing', () => { + // Create a component with no user ID in session storage + sessionStorage.setItem('user', JSON.stringify({})); + + expect(() => { + const newFixture = TestBed.createComponent(FantaComponent); + newFixture.detectChanges(); + }).toThrowError('Invalid user data: missing user ID. User must be logged in to access this component.'); + + // Restore original user + sessionStorage.setItem('user', JSON.stringify(mockUser)); + }); + + it('should throw error when user is not in session storage', () => { + // Remove user from session storage + sessionStorage.removeItem('user'); + + expect(() => { + const newFixture = TestBed.createComponent(FantaComponent); + newFixture.detectChanges(); + }).toThrowError('User not found in session storage. User must be logged in to access this component.'); + + // Restore original user + sessionStorage.setItem('user', JSON.stringify(mockUser)); + }); + }); + + describe('Avatar', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should get avatar source from db service', () => { + const avatarSrc = component.avatarSrc; + expect(mockDbDataService.getAvatarSrc).toHaveBeenCalledWith(mockUser); + expect(avatarSrc).toBe('assets/images/default-avatar.png'); + }); + }); + + describe('FantaVote Object Conversion', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should convert vote array to FantaVote object', () => { + const mockForm = { valid: true } as NgForm; + component.setVoto(1, 1, 1, mockForm); + component.setVoto(1, 2, 2, mockForm); + component.setVoto(1, 3, 3, mockForm); + component.setVoto(1, 4, 4, mockForm); + component.setVoto(1, 5, 5, mockForm); + component.setVoto(1, 6, 6, mockForm); + component.setVoto(1, 7, 7, mockForm); + component.setVoto(1, 8, 8, mockForm); + component.setVoto(1, 9, 1, mockForm); + component.setVoto(1, 10, 2, mockForm); + component.setVoto(1, 11, 1, mockForm); + + const fantaVote = component.getFantaVoteObject(1); + expect(fantaVote.fanta_player_id).toBe(mockUser.id); + expect(fantaVote.username).toBe(mockUser.username); + expect(fantaVote.track_id).toBe(1); + expect(fantaVote.id_1_place).toBe(1); + expect(fantaVote.id_2_place).toBe(2); + expect(fantaVote.constructor_id).toBe(1); + }); + + it('should return empty vote object for non-existent track', () => { + const fantaVote = component.getFantaVoteObject(999); + expect(fantaVote.id_1_place).toBe(0); + expect(fantaVote.id_2_place).toBe(0); + expect(fantaVote.constructor_id).toBe(0); + }); + }); + + describe('Constructors Map', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should populate constructors map from db service', () => { + expect(component.constructors().size).toBe(2); + expect(component.constructors().get(1)).toBe('Red Bull Racing'); + expect(component.constructors().get(2)).toBe('Ferrari'); + }); + }); + + describe('Loading States', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should return false for loading when not loading', () => { + expect(component.isLoading(1)).toBe(false); + }); + + it('should return true for loading when loading', () => { + component['loadingSignal'].set({ 1: true }); + expect(component.isLoading(1)).toBe(true); + }); + }); +}); diff --git a/client/src/app/views/fanta/fanta.component.ts b/client/src/app/views/fanta/fanta.component.ts new file mode 100644 index 000000000..da1fc6fc0 --- /dev/null +++ b/client/src/app/views/fanta/fanta.component.ts @@ -0,0 +1,471 @@ +import { DatePipe } from '@angular/common'; +import { Component, inject, OnInit, signal, computed, ChangeDetectionStrategy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, NgForm } from '@angular/forms'; +import { AccordionComponent, + AccordionItemComponent, + FormModule, + SharedModule, + ButtonDirective, + TemplateIdDirective, + AccordionButtonDirective, + AvatarComponent, + UtilitiesModule, + BadgeComponent} from '@coreui/angular'; +import { AuthService } from './../../service/auth.service'; +import { GridModule } from '@coreui/angular'; +import { DbDataService } from './../../service/db-data.service'; +import { FantaService } from './../../service/fanta.service'; +import { cilFire, cilPowerStandby, cilPeople } from '@coreui/icons'; +import { IconDirective } from '@coreui/icons-angular'; +import { VOTE_INDEX, FORM_STATUS, DRIVER_POSITIONS_COUNT, TOTAL_VOTE_FIELDS, FantaVoteHelper } from '../../model/fanta'; +import { medals, allFlags, posizioni } from '../../model/constants'; +import { LeaderboardComponent } from "../../components/leaderboard/leaderboard.component"; +import { VoteHistoryTableComponent } from '../../components/vote-history-table/vote-history-table.component'; +import { FantaRulesComponent } from '../../components/fanta-rules/fanta-rules.component'; +import { + ButtonCloseDirective, + ModalBodyComponent, + ModalComponent, + ModalHeaderComponent, + ModalTitleDirective, + ThemeDirective +} from '@coreui/angular'; +import type { FantaVote, User, DriverData, TrackData } from '@f123dashboard/shared'; + +@Component({ + selector: 'app-fanta', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + FormsModule, + FormModule, + ButtonDirective, + GridModule, + SharedModule, + AccordionComponent, + AccordionItemComponent, + TemplateIdDirective, + AccordionButtonDirective, + IconDirective, + DatePipe, + AvatarComponent, + UtilitiesModule, + BadgeComponent, + LeaderboardComponent, + VoteHistoryTableComponent, + FantaRulesComponent, + ModalComponent, ModalHeaderComponent, ModalTitleDirective, ThemeDirective, ButtonCloseDirective, ModalBodyComponent + ], + templateUrl: './fanta.component.html', + styleUrl: './fanta.component.scss' +}) +export class FantaComponent implements OnInit { + authService = inject(AuthService); + private dbData = inject(DbDataService); + fantaService = inject(FantaService); + + // Signals for reactive state + private formStatusSignal = signal>({}); + readonly formStatus = this.formStatusSignal.asReadonly(); + + private validationDetailsSignal = signal>({}); + readonly validationDetails = this.validationDetailsSignal.asReadonly(); + + private loadingSignal = signal>({}); + readonly loading = this.loadingSignal.asReadonly(); + + forms: Record = {}; + + private userSignal = signal(null!); + readonly user = this.userSignal.asReadonly(); + + private pilotiSignal = signal([]); + readonly piloti = this.pilotiSignal.asReadonly(); + + private tracksSignal = signal([]); + readonly tracks = this.tracksSignal.asReadonly(); + + private constructorsSignal = signal>(new Map()); + readonly constructors = this.constructorsSignal.asReadonly(); + + private nextTracksSignal = signal([]); + readonly nextTracks = this.nextTracksSignal.asReadonly(); + + private previusTracksSignal = signal([]); + readonly previusTracks = this.previusTracksSignal.asReadonly(); + + // Maps track ID to vote array [place1-8, fastLap, dnf, constructor] + private votazioniSignal = signal>(new Map()); + readonly votazioni = this.votazioniSignal.asReadonly(); + + // Store original loaded data for change detection + private originalVotazioniSignal = signal>(new Map()); + readonly originalVotazioni = this.originalVotazioniSignal.asReadonly(); + + // Computed user fanta points + readonly userFantaPoints = computed(() => + this.fantaService.getFantaPoints(this.user().id) + ); + + private modalRankingVisibleSignal = signal(false); + readonly modalRankingVisible = this.modalRankingVisibleSignal.asReadonly(); + + private modalRulesVisibleSignal = signal(false); + readonly modalRulesVisible = this.modalRulesVisibleSignal.asReadonly(); + + public fireIcon: string[] = cilFire; + public powerIcon: string[] = cilPowerStandby; + public teamIcon: string[] = cilPeople; + public posizioni = posizioni; + public medals = medals; + public allFlags = allFlags; + + ngOnInit(): void { + this.loadUserFromSession(); + this.loadDriversTracksAndConstructors(); + this.filterTracks(); + this.loadPreviousVotes(); + } + + /** + * Loads user data from session storage. + */ + private loadUserFromSession(): void { + const userString = sessionStorage.getItem('user'); + if (!userString) { + throw new Error('User not found in session storage. User must be logged in to access this component.'); + } + const user = JSON.parse(userString) as User; + if (!user.id) { + throw new Error('Invalid user data: missing user ID. User must be logged in to access this component.'); + } + this.userSignal.set(user); + } + + /** + * Loads drivers, tracks, and constructors from database service. + */ + private loadDriversTracksAndConstructors(): void { + this.pilotiSignal.set(this.dbData.allDrivers()); + this.tracksSignal.set(this.dbData.tracks()); + const constructorsMap = new Map(); + this.dbData.constructors().forEach(constructor => { + constructorsMap.set(constructor.constructor_id, constructor.constructor_name); + }); + this.constructorsSignal.set(constructorsMap); + } + + /** + * Filters tracks into upcoming and previous races. + */ + private filterTracks(): void { + const today = new Date(); + + this.nextTracksSignal.set( + this.tracks() + .filter(item => new Date(item.date) > today) + .slice(0, 4) + ); + + this.previusTracksSignal.set( + this.tracks() + .filter(item => new Date(item.date) <= today) + ); + } + + /** + * Loads previously saved votes for all tracks. + */ + private loadPreviousVotes(): void { + this.previusTracks().forEach(track => this.applyPreviousVote(track)); + this.nextTracks().forEach(track => this.applyPreviousVote(track)); + } + + /** + * Applies previously saved vote data to a track. + */ + private applyPreviousVote(track: TrackData): void { + const previousVote = this.fantaService.getFantaVote(this.user().id, track.track_id); + if (!previousVote) {return;} + + // Convert Fanta object to array using helper + const previousVoteArray = Array.from(FantaVoteHelper.toArray(previousVote)); + + // Update signals immutably + this.votazioniSignal.update(map => { + const newMap = new Map(map); + newMap.set(track.track_id, previousVoteArray); + return newMap; + }); + + this.originalVotazioniSignal.update(map => { + const newMap = new Map(map); + newMap.set(track.track_id, [...previousVoteArray]); + return newMap; + }); + } + + /** + * Publishes a vote for a specific track. + * @param trackId The track identifier + * @param form The form reference for validation + */ + async publishVoto(trackId: number, form: NgForm): Promise { + this.forms[trackId] = form; + + const validation = this.validateForm(trackId); + if (!validation.isValid) { + this.formStatusSignal.update(status => ({ ...status, [trackId]: FORM_STATUS.VALIDATION_ERROR })); + this.validationDetailsSignal.update(details => ({ + ...details, + [trackId]: { hasEmptyVotes: validation.hasEmptyVotes, hasDuplicates: validation.hasDuplicates } + })); + return; + } + + // Set loading state + this.loadingSignal.update(loading => ({ ...loading, [trackId]: true })); + + const votes = this.votazioni().get(trackId) || []; + const fantaVoto: FantaVote = { + fanta_player_id: this.user().id, + username: this.user().username, + track_id: trackId, + id_1_place: votes[VOTE_INDEX.PLACE_1], + id_2_place: votes[VOTE_INDEX.PLACE_2], + id_3_place: votes[VOTE_INDEX.PLACE_3], + id_4_place: votes[VOTE_INDEX.PLACE_4], + id_5_place: votes[VOTE_INDEX.PLACE_5], + id_6_place: votes[VOTE_INDEX.PLACE_6], + id_7_place: votes[VOTE_INDEX.PLACE_7], + id_8_place: votes[VOTE_INDEX.PLACE_8], + id_fast_lap: votes[VOTE_INDEX.FAST_LAP], + id_dnf: votes[VOTE_INDEX.DNF], + constructor_id: votes[VOTE_INDEX.CONSTRUCTOR] + }; + + try { + await this.fantaService.setFantaVote(fantaVoto); + this.formStatusSignal.update(status => ({ ...status, [trackId]: FORM_STATUS.SUCCESS })); + + // Update the original votazioni to reflect the saved state + const currentVotes = this.votazioni().get(trackId); + if (currentVotes) { + this.originalVotazioniSignal.update(map => { + const newMap = new Map(map); + newMap.set(trackId, [...currentVotes]); + return newMap; + }); + } + } catch (error) { + console.error('Error saving fantasy vote:', error); + this.formStatusSignal.update(status => ({ ...status, [trackId]: FORM_STATUS.SAVE_ERROR })); + } finally { + // Clear loading state + this.loadingSignal.update(loading => ({ ...loading, [trackId]: false })); + } + } + + /** + * Validates if a vote form is complete and valid. + * @param trackId The track identifier + * @returns Validation result with specific error details + */ + private validateForm(trackId: number): { isValid: boolean; hasEmptyVotes: boolean; hasDuplicates: boolean } { + const votoArray = this.votazioni().get(trackId) || []; + + // Check if all required fields are present + if (votoArray.length < TOTAL_VOTE_FIELDS) { + return { isValid: false, hasEmptyVotes: true, hasDuplicates: false }; + } + + // Check for empty votes in all required positions + const hasEmptyVotes = votoArray.some((v, i) => i <= VOTE_INDEX.CONSTRUCTOR && v === 0); + + // Check for duplicates only in driver positions (indices 0-7) + const driverVotes = votoArray.slice(0, DRIVER_POSITIONS_COUNT); + const hasDuplicates = driverVotes.some((v, i) => driverVotes.indexOf(v) !== i); + + return { + isValid: !hasEmptyVotes && !hasDuplicates, + hasEmptyVotes, + hasDuplicates + }; + } + + /** + * Checks if a vote form is complete and valid. + * @param trackId The track identifier + * @returns true if form is valid, false otherwise + */ + formIsValid(trackId: number): boolean { + return this.validateForm(trackId).isValid; + } + + /** + * Gets a specific vote value for a track. + * @param trackId The track identifier + * @param index 1-based index (1-11) + * @returns The vote value or 0 if not found + */ + getVoto(trackId: number, index: number): number { + const votoArray = this.votazioni().get(trackId) || []; + return votoArray[index - 1] || 0; + } + + /** + * Gets the position where a driver was voted. + * @param trackId The track identifier + * @param pilota The driver ID + * @returns The position (1-8) or 0 if not found + */ + getVotoPos(trackId: number, pilota: number): number { + const votoArray = this.votazioni().get(trackId) || []; + const posizione = votoArray.indexOf(pilota); + return posizione >= 0 ? posizione + 1 : 0; + } + + /** + * Gets all votes for a track. + * @param trackId The track identifier + * @returns Array of votes or undefined + */ + getVoti(trackId: number): number[] | undefined { + return this.votazioni().get(trackId); + } + + /** + * Sets a specific vote value for a track. + * @param trackId The track identifier + * @param index 1-based index (1-11) + * @param valore The vote value + * @param form The form reference + */ + setVoto(trackId: number, index: number, valore: number, form: NgForm): void { + this.forms[trackId] = form; + + if (!valore) {return;} + + // Update votazioni signal immutably + this.votazioniSignal.update(votazioniMap => { + const newMap = new Map(votazioniMap); + let votoArray = newMap.get(trackId); + + if (!votoArray) { + votoArray = []; + } else { + votoArray = [...votoArray]; // Create a copy + } + + votoArray[index - 1] = +valore; + newMap.set(trackId, votoArray); + return newMap; + }); + + // Reset form status and validation details when data is changed + if (this.formStatus()[trackId] === FORM_STATUS.SUCCESS) { + this.formStatusSignal.update(status => { + const newStatus = { ...status }; + delete newStatus[trackId]; + return newStatus; + }); + } + + // Clear validation details when user makes changes + if (this.validationDetails()[trackId]) { + this.validationDetailsSignal.update(details => { + const newDetails = { ...details }; + delete newDetails[trackId]; + return newDetails; + }); + } + } + + toggleModalRanking() { + this.modalRankingVisibleSignal.update(value => !value); + } + + toggleModalRules() { + this.modalRulesVisibleSignal.update(value => !value); + } + + /** + * Checks if there are unsaved changes for a track. + */ + hasUnsavedData(trackId: number): boolean { + const currentVotes = this.votazioni().get(trackId) || []; + const originalVotes = this.originalVotazioni().get(trackId) || []; + + // If form was just saved successfully, no unsaved data + if (this.formStatus()[trackId] === FORM_STATUS.SUCCESS) { + return false; + } + + // Check if there are any votes entered + const hasVotes = currentVotes.some(vote => vote && vote > 0); + + // If no votes at all, no unsaved data + if (!hasVotes) {return false;} + + // If there are no original votes but current votes exist, it's unsaved + if (originalVotes.length === 0) {return true;} + + // Compare current votes with original votes + if (currentVotes.length !== originalVotes.length) {return true;} + + // Check if any vote has changed + return currentVotes.some((vote, i) => vote !== originalVotes[i]); + } + + /** + * Checks if no vote data exists for a track. + */ + hasNoData(trackId: number): boolean { + const currentVotes = this.votazioni().get(trackId) || []; + + // If form is already saved, don't show "Votazione Mancante" + if (this.formStatus()[trackId] === FORM_STATUS.SUCCESS) { + return false; + } + + // Show "Votazione Mancante" if no votes are entered at all + const hasAnyVotes = currentVotes.some(vote => vote && vote > 0); + return !hasAnyVotes; + } + + get avatarSrc(): string { + return this.dbData.getAvatarSrc(this.user()); + } + + /** + * Checks if a form is currently being submitted. + */ + isLoading(trackId: number): boolean { + return this.loading()[trackId] || false; + } + + /** + * Convert vote array to FantaVote object for the reusable component + */ + getFantaVoteObject(trackId: number): FantaVote { + const voteArray = this.votazioni().get(trackId) || []; + return { + fanta_player_id: this.user().id, + username: this.user().username, + track_id: trackId, + id_1_place: voteArray[VOTE_INDEX.PLACE_1] || 0, + id_2_place: voteArray[VOTE_INDEX.PLACE_2] || 0, + id_3_place: voteArray[VOTE_INDEX.PLACE_3] || 0, + id_4_place: voteArray[VOTE_INDEX.PLACE_4] || 0, + id_5_place: voteArray[VOTE_INDEX.PLACE_5] || 0, + id_6_place: voteArray[VOTE_INDEX.PLACE_6] || 0, + id_7_place: voteArray[VOTE_INDEX.PLACE_7] || 0, + id_8_place: voteArray[VOTE_INDEX.PLACE_8] || 0, + id_fast_lap: voteArray[VOTE_INDEX.FAST_LAP] || 0, + id_dnf: voteArray[VOTE_INDEX.DNF] || 0, + constructor_id: voteArray[VOTE_INDEX.CONSTRUCTOR] || 0 + }; + } + +} diff --git a/src/app/views/charts/routes.ts b/client/src/app/views/fanta/routes.ts similarity index 50% rename from src/app/views/charts/routes.ts rename to client/src/app/views/fanta/routes.ts index 0ab61e5a7..c3123a334 100644 --- a/src/app/views/charts/routes.ts +++ b/client/src/app/views/fanta/routes.ts @@ -1,13 +1,11 @@ import { Routes } from '@angular/router'; -import { ChartsComponent } from './charts.component'; - export const routes: Routes = [ { path: '', - component: ChartsComponent, + loadComponent: () => import('./fanta.component').then((m) => m.FantaComponent), data: { - title: 'Charts' + title: $localize`Fanta` } } ]; diff --git a/client/src/app/views/piloti/piloti.component.html b/client/src/app/views/piloti/piloti.component.html new file mode 100644 index 000000000..d863d5d59 --- /dev/null +++ b/client/src/app/views/piloti/piloti.component.html @@ -0,0 +1,42 @@ +@if (!isLoading()) { + + + + + + + + + + @for (pilota of piloti(); track pilota.driver_id; let i = $index) { + + + + + } + + + + + + @for (constructor of constructors(); track constructor.constructor_id; let i = $index) { + + + + + } + + + + +} @else { +
+ + ASPETTA STRONZO + +
+} \ No newline at end of file diff --git a/client/src/app/views/piloti/piloti.component.scss b/client/src/app/views/piloti/piloti.component.scss new file mode 100644 index 000000000..e399af678 --- /dev/null +++ b/client/src/app/views/piloti/piloti.component.scss @@ -0,0 +1,49 @@ +:host { + display: block; + width: 100%; + overflow-x: hidden; +} + +:host ::ng-deep { + .body { + overflow: auto; + } + + .tab-content { + width: 100%; + overflow-x: hidden; + } + + c-tab-panel { + width: 100%; + overflow-x: hidden; + } +} + +// Variabili per la personalizzazione rapida +$grid-gutter-size: 0.1rem; // Modifica questo valore per cambiare lo spazio tra le card + +// Override dello spacing della griglia +:host ::ng-deep { + c-row { + margin: 0; + width: 100%; + display: flex; + flex-wrap: wrap; + + > c-col { + padding: #{$grid-gutter-size} !important; + } + } +} + +// Stili per il loading spinner +.loading-container { + text-align: center; + padding-top: calc(100vh / 2); + height: 100vh; + + .spinner-border { + margin: 0 0.5rem; + } +} \ No newline at end of file diff --git a/client/src/app/views/piloti/piloti.component.spec.ts b/client/src/app/views/piloti/piloti.component.spec.ts new file mode 100644 index 000000000..6493de847 --- /dev/null +++ b/client/src/app/views/piloti/piloti.component.spec.ts @@ -0,0 +1,26 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; + + +import { PilotiComponent } from './piloti.component'; + +describe('PilotiComponent', () => { + let component: PilotiComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [provideNoopAnimations()], + imports: [PilotiComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PilotiComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/client/src/app/views/piloti/piloti.component.ts b/client/src/app/views/piloti/piloti.component.ts new file mode 100644 index 000000000..dbafe3c22 --- /dev/null +++ b/client/src/app/views/piloti/piloti.component.ts @@ -0,0 +1,57 @@ +import { Component, OnInit, ChangeDetectionStrategy, signal, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { DbDataService } from '../../service/db-data.service'; +import { ConstructorService } from '../../service/constructor.service'; +import { + ColComponent, + RowComponent, + Tabs2Module, +} from '@coreui/angular'; +import type { Constructor, DriverData } from '@f123dashboard/shared'; +import { PilotCardComponent } from '../../components/pilot-card/pilot-card.component'; +import { ConstructorCardComponent } from '../../components/constructor-card/constructor-card.component'; + +@Component({ + selector: 'app-cards', + templateUrl: './piloti.component.html', + styleUrls: ['./piloti.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + RowComponent, + ColComponent, + Tabs2Module, + PilotCardComponent, + ConstructorCardComponent +] +}) + +export class PilotiComponent implements OnInit { + private dbData = inject(DbDataService); + private constructorService = inject(ConstructorService); + + piloti = signal([]); + constructors = signal([]); + isLoading = signal(true); + + ngOnInit() { + try { + this.isLoading.set(true); + + // Fetch data from service + const drivers = this.dbData.allDrivers(); + const constructorsData = this.dbData.constructors(); + + // Calculate points for constructors based on drivers data + const constructorsWithPoints = this.constructorService.calculateConstructorPoints(constructorsData, drivers); + + this.piloti.set(drivers); + this.constructors.set(constructorsWithPoints); + } catch (error) { + console.error('Error loading data:', error); + } finally { + this.isLoading.set(false); + } + } + +} diff --git a/src/app/views/widgets/routes.ts b/client/src/app/views/piloti/routes.ts similarity index 50% rename from src/app/views/widgets/routes.ts rename to client/src/app/views/piloti/routes.ts index 0b4039a4e..474c9c217 100644 --- a/src/app/views/widgets/routes.ts +++ b/client/src/app/views/piloti/routes.ts @@ -3,9 +3,10 @@ import { Routes } from '@angular/router'; export const routes: Routes = [ { path: '', - loadComponent: () => import('./widgets/widgets.component').then(m => m.WidgetsComponent), + loadComponent: () => import('./piloti.component').then(m => m.PilotiComponent), data: { - title: 'Widgets' + title: $localize`Piloti` } } ]; + diff --git a/client/src/app/views/playground/playground.component.html b/client/src/app/views/playground/playground.component.html new file mode 100644 index 000000000..2cd9f2b3d --- /dev/null +++ b/client/src/app/views/playground/playground.component.html @@ -0,0 +1,117 @@ + + + + + @if (!isLoggedIn()) { +
+ + +
Effettua il login per salvare il tuo punteggio
+
+
+ } +
+ + +

+ Tocca lo schermo o clicca per iniziare. Tocca o clicca di nuovo quando le luci si spengono. +

+
+
+ + + +
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+ + +

{{ playerStatus() }}

+
+
+ @if (playerScore()) { + + +

Il tuo miglior punteggio è: {{ playerBestScore() }} ms

+
+
+ } +
+ + + + + + + + + + @if (showColumn()) { + + } + + + + @for (leaderboardBestScore of playgroundLeaderboard().slice(0, 13); + track leaderboardBestScore.user_id; + let i = $index) { + + + + + + @if (showColumn()) { + + } + + } + +
Pos.UsernamePunteggio [ms]Data
+
#{{i + 1}}
+
+
{{ leaderboardBestScore.username }}
+
+
+ + +
+ {{ leaderboardBestScore.best_score }} +
+
+
+ {{ leaderboardBestScore.best_date | date:'dd/MM/yyyy' }} +
+
+
+
diff --git a/client/src/app/views/playground/playground.component.scss b/client/src/app/views/playground/playground.component.scss new file mode 100644 index 000000000..d8c77d70c --- /dev/null +++ b/client/src/app/views/playground/playground.component.scss @@ -0,0 +1,123 @@ +/** LIGHTS SECTION **/ +#lights_container { + width: 210; + height: fit-content; + display: inline-flex; +} + +.bulbs_container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin-right: 10px; + height: 120px; + width: 55px; + background-color: rgb(61, 61, 61); + border-radius: 50px; +} + +.bulb_up { + background-color: rgb(34, 34, 34); + width: 50px; + height: 50px; + margin-bottom: 10px; + border-radius: 50%; + border: 3px solid black; +} + +.bulb { + background-color: rgb(34, 34, 34); + width: 50px; + height: 50px; + border-radius: 50%; + border: 3px solid black; + border-top: 13px solid black; +} + +.red_light { + background-color: red; + transition: all 0 s; +} + +.logo { + background-color: transparent; + background-size: contain; + background-image: url('../../../assets/images/logo_raceforfederica.png'); + transition: all 0 s; +} + +.fade.show { + display: flex; + justify-content: center; + /* Horizontal centering */ + align-items: center; + /* Vertical centering */ + text-align: center; + /* Center text inside the div */ + height: 100vh; + /* Full viewport height */ +} + +.lights-wrapper { + display: flex; + flex-direction: column; + justify-content: center; /* center vertically */ + align-items: center; /* center horizontally */ + text-align: center; +} + +.lights-wrapper c-row { + margin-bottom: 2rem; /* spacing between components */ +} + +.lights-wrapper .lights-help { + font-size: 1.2rem; + font-style: italic; + color: #666; +} + +.lights-wrapper .lights-result { + font-size: 3rem; + font-weight: 800; + color: #dc3545; /* red for example */ + letter-spacing: 2px; +} + + +/* TEXT SECTIONS */ +.lights-help { + font-style: italic; + font-size: 0.9rem; /* smaller than normal text */ + opacity: 0.9; /* optional: slightly softer look */ +} + +.lights-score { + font-size: 3rem; /* make it large */ + font-weight: 800; /* extra bold */ + text-transform: uppercase; /* optional: ensure all caps */ + letter-spacing: 2px; /* optional: space out the letters slightly */ + text-align: center; /* center it */ +} + +.lights-best-score { + font-style: italic; + font-size: 0.9rem; /* smaller than normal text */ + opacity: 0.9; /* optional: slightly softer look */ +} + +/* TABLE SECTION */ +.custom-table { + border-collapse: separate; + border-spacing: 0; + overflow: hidden; /* Nasconde i bordi sporgenti */ +} + +.table-wrapper { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + height: 100%; +} \ No newline at end of file diff --git a/client/src/app/views/playground/playground.component.spec.ts b/client/src/app/views/playground/playground.component.spec.ts new file mode 100644 index 000000000..0f1799aff --- /dev/null +++ b/client/src/app/views/playground/playground.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; + +import { PlaygroundComponent } from './playground.component'; + +describe('PlaygroundComponent', () => { + let component: PlaygroundComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [provideNoopAnimations(), ], + imports: [PlaygroundComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PlaygroundComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/views/playground/playground.component.ts b/client/src/app/views/playground/playground.component.ts new file mode 100644 index 000000000..bdd63b0c4 --- /dev/null +++ b/client/src/app/views/playground/playground.component.ts @@ -0,0 +1,210 @@ +import { Component, AfterViewInit, OnInit, ChangeDetectionStrategy, inject, signal, computed } from '@angular/core'; +import { DatePipe, CommonModule } from '@angular/common'; +import { GridModule, TableDirective, AvatarComponent, AlertComponent, ColorModeService } from '@coreui/angular'; +import { cilPeople, cilWarning } from '@coreui/icons'; +import { IconDirective } from '@coreui/icons-angular'; +import { PlaygroundService, PlaygroundBestScore } from '../../service/playground.service'; +import { AuthService } from 'src/app/service/auth.service'; + + +@Component({ + selector: 'app-playground', + imports: [ + GridModule, + TableDirective, + IconDirective, + AvatarComponent, + AlertComponent, + DatePipe, + CommonModule + ], + templateUrl: './playground.component.html', + styleUrls: ['./playground.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PlaygroundComponent implements OnInit, AfterViewInit { + + playgroundService = inject(PlaygroundService); + private authService = inject(AuthService); + readonly #colorModeService = inject(ColorModeService); + + public screenWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; + + // Use the service's signal directly + playgroundLeaderboard = this.playgroundService.playgroundLeaderboard; + currentUser = this.authService.currentUser; + isLoggedIn = computed(() => this.authService.isAuthenticated()); + + public cilPeople: string[] = cilPeople; + public cilWarning: string[] = cilWarning; + + bulbsUp: NodeListOf | null = null; + bulbs: NodeListOf | null = null; + isTimerStarted = signal(false); + isLightsTriggered = signal(false); + hasLightsError = signal(false); + timerStartTime: number | null = null; + playerStatus = signal(""); + playerStatusColor = signal(""); + playerScore = signal(null); + playerBestScore = signal(9999); + hasJumpStart = signal(false); + + ngOnInit(): void { + // Initialize best score for logged-in users + if (this.isLoggedIn() && this.currentUser()?.id) { + this.playerBestScore.set(this.playgroundService.getUserBestScore(this.currentUser()!.id)); + } + this.resetGameText(); + } + + + ngAfterViewInit(): void { + this.bulbsUp = document.querySelectorAll('.bulb_up'); + this.bulbs = document.querySelectorAll('.bulb'); + } + + showColumn(): boolean { + return this.screenWidth > 1600; + } + + gameTrigger(): void { + if (!this.isTimerStarted()) { + if (this.hasLightsError()) { + return; + } + + if (this.isLightsTriggered()) { + // Jump start condition + this.playerStatus.set(`FALSA PARTENZA`); + this.playerStatusColor.set("#FF0000"); + this.isLightsTriggered.set(false); + this.hasJumpStart.set(true); + this.lightsError(); + return; + } + + // Start light up sequence + this.resetGameText(); + this.isLightsTriggered.set(true); + this.lightsUp(); + return; + } + + // Record reaction time and game over + const elapsedTime = Date.now() - (this.timerStartTime ?? 0); + this.playerScore.set(elapsedTime); + this.playerStatus.set(`Tempo di reazione: ${this.playerScore()} ms`); + this.playerStatusColor.set("#0E8F5F"); + + this.isTimerStarted.set(false); + this.timerStartTime = null; + this.isLightsTriggered.set(false); + + const playerScoreValue = this.playerScore(); + if (playerScoreValue === null) { + return; + } + + // Update best score if current score is better + if (playerScoreValue < this.playerBestScore()) { + this.playerBestScore.set(playerScoreValue); + + // Save to backend if user is logged in + const currentUserValue = this.currentUser(); + if (this.isLoggedIn() && currentUserValue?.id) { + const newBestScore: PlaygroundBestScore = { + user_id: currentUserValue.id, + username: currentUserValue.username, + image: currentUserValue.image ?? "", + best_score: playerScoreValue, + best_date: new Date(), + }; + this.playgroundService.setUserBestScore(newBestScore); + } + } + } + + async lightsUp(): Promise { + const bulbs = this.bulbs ?? document.querySelectorAll('.bulb'); + + for (const bulb of Array.from(bulbs)) { + if (this.hasJumpStart()) { + break; + } + bulb.classList.add('red_light'); + await sleep(1000); + } + + if ( !this.hasJumpStart() ) { + const maxRandomDelay = 4000; + const minRandomDelay = 500; + const randomDelay = Math.floor(Math.random() * (maxRandomDelay - minRandomDelay + 1)) + minRandomDelay; + await sleep(randomDelay); + } + + if ( !this.hasJumpStart() ) { + this.lightsOut(); + } + + this.hasJumpStart.set(false); + } + + lightsOut(): void { + const bulbs = this.bulbs ?? document.querySelectorAll('.bulb'); + + for (const bulb of Array.from(bulbs)) { + bulb.classList.remove('red_light'); + } + + this.isTimerStarted.set(true); + this.timerStartTime = Date.now(); + } + + async lightsError(): Promise { + const bulbs = this.bulbs ?? document.querySelectorAll('.bulb'); + + this.hasLightsError.set(true); + + for (let i = 0; i < 3; i++) { + for (const bulb of Array.from(bulbs)) { + bulb.classList.add('red_light'); + } + + await sleep(500); + + for (const bulb of Array.from(bulbs)) { + bulb.classList.remove('red_light'); + } + + await sleep(500); + } + + this.resetGameText(); + + this.hasLightsError.set(false); + } + + getAvatar(userId: number, image?: string): string { + if (image) + {return `data:image/jpeg;base64,${image}`;} + + // Fallback to file path + return `./assets/images/avatars_fanta/${userId}.png`; + } + + resetGameText(): void { + this.playerStatus.set(`SEI PRONTO?`); + if ( this.#colorModeService.colorMode.name === 'dark' ) { + this.playerStatusColor.set("#FFFFFF"); + } else { + this.playerStatusColor.set("#000000"); + } + } + + +} + +function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/client/src/app/views/playground/routes.ts b/client/src/app/views/playground/routes.ts new file mode 100644 index 000000000..824e8d14b --- /dev/null +++ b/client/src/app/views/playground/routes.ts @@ -0,0 +1,12 @@ +import { Routes } from '@angular/router'; +import { PlaygroundComponent } from './playground.component'; + +export const routes: Routes = [ + { + path: '', + component: PlaygroundComponent, + data: { + title: 'Playground' + } + } +]; diff --git a/client/src/app/views/regole/regole.component.html b/client/src/app/views/regole/regole.component.html new file mode 100644 index 000000000..73b8558cb --- /dev/null +++ b/client/src/app/views/regole/regole.component.html @@ -0,0 +1,287 @@ +
+ +
+
+

Regolamento Ufficiale Campionato 📜

+

Seguire attentamente le regole per garantire una competizione corretta e divertente per tutti.

+
+
+ +
+ +
+
+
+ Calendario Campionato e Formato +
+
+
    +
  • Giorno del campionato: ogni lunedì sera
  • +
  • Orario di inizio: dalle 21:45 (puntuali)
  • +
  • Formato: 2 GP per serata, con qualifiche e gara (+sprint) per ogni GP
  • +
  • Meteo: dinamico, e se bisogna rifare le gara si mette lo stesso meteo
  • +
  • Scuderie Pilota: prestazioni reali, e scuderie assegnate inversamente all'abilità del pilota. Da stabilire all'inizio, ma possono subire variazioni
  • +
  • Durata prove libere: dalla fine dei GP precedenti, alle 23:59 del giorno prima del GP (domenica)
  • +
  • Durata qualifiche: sessione unica, dalla durata di 17 min (circa 3 giri lanciati)
  • +
  • Durata sprint: gara 5 giri
  • +
  • Durata gara: gara 35% (se GP sprint, gara 25%)
  • +
+
+
+
+
+ +
+
+
+
+ Punteggi Campionato +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PosizioneGaraSprintQualificheProve Libere
251075
21864
18753
14642
11431
8320
5210
2100
DNF00
Giro veloce21
+
+
+ +
+
+
+ + +
+ +
+
+
+ Penalità, Comportamento in Pista e Regole Generali +
+
+
Regole generali
+
    +
  • Prova video: per tutte le casistiche sottostanti, la prova video è MOLTO importante. Senza non è garantito che le regole vengano applicate, a discrezione della FIA
  • +
  • Votazioni: quando non è possibile stabilire in maniera ovvia se una regola è stata violata o no (esempio per i contatti) è prevista una votazione. Nel caso di parità di voti, la violazione / penalità viene convalidata
  • +
  • Zone grigie del regolamento: la decisione è della FIA dopo votazione, e la regola aggiuntiva viene resa valida dalla giornata seguente
  • +
+
Regole / contatti qualifica
+
    +
  • Ostruzione in qualifiche: arretramento 1 posizione in griglia
  • +
  • Contatto con colpa giro uscita, danno terzo parziale: arretramento 1 posizioni in griglia
  • +
  • Contatto con colpa giro lanciato o dopo ultima curva giro uscita, danno terzo parziale: arretramento 1 posizioni qualifica, quindi anche in griglia
  • +
  • Contatto con colpa, danno terzo totale: arretramento in ultima posizione in qualifica, quindi anche in griglia
  • +
+
Regole / contatti in gara
+
    +
  • Contatto con colpa parziale, danno terzo parziale, tempo perso minimo: penalità di 5 sec,
  • +
  • Contatto con colpa parziale, danno terzo terminale: penalità di 10 sec e eventuale -1 punto patente
  • +
  • Contatto con colpa parziale e sorpasso, no danno terzo, tempo perso minimo: restituire posizione entro 1 giro, se no 5 sec a fine gara
  • +
  • Contatto con colpa parziale, no danno o danno terzo parziale, tempo perso significativo (spin/fuori pista): penalità di 10 sec
  • +
  • Contatto con colpa totale, danno terzo parziale, tempo perso minimo: penalità di 5 sec
  • +
  • Contatto con colpa totale, danno terzo terminale: penalità di 20 sec e eventuale -1 punto patente
  • +
  • Contatto con colpa totale e sorpasso, no danno terzo, tempo perso minimo: restituire posizione entro 1 giro, se no 5 a fine gara
  • +
  • Contatto con colpa totale, no danno o danno terzo parziale, tempo perso significativo (spin/fuori pista): penalità di 10 sec
  • +
  • Danno non terminale per 2o+ piloti nelle prime 2 curve della gara (non sprint): ricominciare (max 1 volta)
  • +
  • Pilota abbandona volontariamente la gara, senza aver riportato un danno terminale (per rabbia o altro): DNF
  • +
  • Danno terminale per 1o+ piloti nelle prime 2 curve della gara: ricominciare (max 1 volta)
  • +
  • Danno non terminale per 1 pilota nelle prime 2 curve della gara: si va avanti
  • +
  • Danno terminale o non 1o+ piloti in qualifica: si va avanti
  • +
  • Contatto con pilota davanti più lento: il pilota davanti, a meno di movimenti irregolari volontari, ha tutto il diritto di essere lento. La colpa è di quello dietro, seguendo le regole sopracitate
  • +
  • Assetto sbagliato per il GP: si va avanti
  • +
  • Difesa multipla: penalità di 5 secondi, se senza danni. Se con danni, si applicano le regole sopra (la colpa è del pilota davanti)
  • +
  • Move under braking: se con danni o tempo perso, si applicano le regole sopra. La penalità può essere per chi fa move under braking, o per chi segue se per esempio sbaglia la staccata (si valuta caso per caso)
  • +
  • Sorpasso fuori pista: restituire posizione entro 1 giro, se no 5 sec a fine gara. 20 sec e 1 punto patente se si taglia volontariamente senza un motivo valido (spin, contatto, ecc)
  • +
  • Rientro pericoloso in pista: si seguono le regole sopracitate, però la colpa è di quello che rientra in pista. Se non c'è contatto ma tempo perso minimo, 5 secondi
  • +
  • Sorpasso in curva: bisogna sempre lasciare spazio se le macchine sono affiancate all’ingresso curva (gomma anteriore macchina dietro ad altezza pilota macchina davanti), se no il sorpasso non è valido e le regole sopra devono essere applicate per il pilota con colpa
  • +
+
Patente
+
    +
  • 1o+ danni ripetuti e/o gravi in una stessa serata di gare: -1 punto sulla patente, dopo voto con o senza prova video (si parte da 3 punti)
  • +
  • Pilota finisce i punti sulla patente: salta 1 GP (tutte le sessioni) e la volta dopo inizia da 1 punto in meno sulla patente
  • +
  • Pilota causa o prova a causare un incidente terminale volontariamente (per rabbia o altro): rimozione di tutti i punti dalla patente, e relativa penalità
  • +
+
Problemi gioco in qualifica
+
    +
  • Penalità date dal gioco: tutte valide (tranne se davvero ingiuste e rimovibili a fine qualifica)
  • +
  • Problemi di connessione / corrente / gioco / esterni durante qualifiche 1o+ piloti: la sessione va avanti fino alla fine, provando a rientrare nella sessione. I tempi già fatti sono validi. Se non sono stati fatti tempi, vengolo usate le ultime posizioni disponibili (tempi delle prove libere per risolvere i pareggi)
  • +
  • Problemi di connessione / gioco per 1o+ piloti durante la creazione lobby: riprovare fino a quando non vengono risolte
  • +
+
Problemi gioco in gara
+
    +
  • Penalità date dal gioco: tutte valide (tranne se davvero ingiuste e rimovibili a fine gara, con prova video)
  • +
  • Problemi di connessione / corrente / gioco / esterni 1o+ piloti: durante il 1° giro, ricominciare (max 1 volta), poi DNF
  • +
  • Problemi di connessione / corrente / gioco / esterni 1o2 piloti: dopo il 1° giro punti delle ultime posizioni disponibili per i piloti con problemi, nell'ordine corrente
  • +
  • Problemi di connessione / corrente / gioco / esterni 3o+ piloti: dopo il 1° giro e prima del 75% di gara, gara da rifare con posizioni di partenza correnti con numero di giri rimanenti (o vicino); dopo il 75% di gara, si usa posizioni correnti come classifica
  • +
  • Problemi di connessione / gioco per 1o+ piloti che causa contatto, danno terzo parziale e/o tempo perso minimo: si va avanti (valido per tutta la gara), no penalità
  • +
  • Problemi di connessione / gioco per 1o+ piloti che causa contatto, danno terzo terminale o tempo perso significativo: si ricomincia (max 1 volta), no penalità
  • +
  • La gara deve essere continuata in un secondo momento (non rifatta da zero): la configurazione di macchine, posizioni, pista e meteo non può essere cambiata. in caso di non rispetto DNF
  • +
  • Problemi in momenti diversi della gara: il numero di piloti con problemi può essere sommato e può, al raggiungimento del numero minimo di piloti per una regola, portare a ricominciare la gara (esempio: 1 pilota si disconnette al 50% di gara, la gara va avanti; ma se 2 piloti poi si disconnettono al 60% di gara, allora la gara si ricomincia dal 60%, con tutti. Chi è stato disconnesso prima parte nelle ultime posizioni disponibili)
  • +
+
Richiesta VAR (Video Assisted Ricominciare)
+
    +
  • Richiesta di VAR corretta: si seguono le regole
  • +
  • Richiesta di VAR scorretta con pilota ancora in sessione: DNF sessione corrente
  • +
  • Richiesta di VAR scorretta con pilota fuori dalla sessione: DNF sessione corrente e esclusione qualifiche prossimo GP se esistono (parte ultimo)
  • +
  • Uso improprio della chat vocale: lavori socialmente utili in Ruanda
  • +
+

LEGENDA

+
    +
  • Colpa parziale: in ingresso curva ruota anteriore di chi segue al livello o davanti alla ruota posteriore di chi predece, ma dietro ad altezza pilota, frenata controllata
  • +
  • Colpa totale: in ingresso curva ruota anteriore di chi segue dietro alla ruota posteriore di chi precede, frenata non controllata / bloccato da lontano, oppure break check
  • +
  • Danno parziale: qualunque danno che ti permetta di continuare la gara
  • +
  • Danno totale: morto
  • +
  • Tempo perso minimo: 1/2/3 sec persi ma non posizioni dagli altri piloti (non parte dell'incidente)
  • +
  • Tempo perso significativo: >4 sec persi e eventualmente posizioni dagli altri piloti (non parte dell'incidente)
  • +
  • Richiesta VAR: durante la gara è possibile richiedere alla FIA di ricominciare la gara. Tutti i piloti hanno il diritto di farlo quante volte si vuole, se si crede che una delle regole sopracitate lo permetta. La richiesta deve essere effettuata entro 2 giri dal fatto incriminante. Nota: questo vale solamente per le regole che fanno ricominciare la sessione (non per penalità, punti patente, ecc, che vengono valutati a fine gara)
  • +
  • Difesa multipla: quando il pilota davanti esegue più di 1 cambio di traiettoria per difendersi
  • +
  • Move under braking: quando il pilota davanti esegue un cambio di traiettoria a frenata iniziata per difendersi, a meno che non stia seguendo la traiettoria ideale
  • +
+
+
+
+
+ +
+ +
+
+
+ Assenze e sanzioni +
+
+
    +
  • Assenza giustificata: notificata alla FIA entro le 23:59 del venerdì prima del GP, cambio data GP (max 1 volta per pilota a stagione)
  • +
  • Assenza ingiustificata: DNF
  • +
  • Arrivo in ritardo alla giornata giustificato: max 15 minuti dopo (22:00)
  • +
  • Arrivo in ritardo alla giornata ingiustificato: si saltano tutte le sessioni fino a quando arriva
  • +
+
+
+
+ +
+
+
+ Incidenti e Reclami +
+
+

I piloti possono presentare reclami ufficiali entro 24 ore dalla gara, allegando video o prove. I commissari valuteranno l'incidente e assegneranno penalità se necessario.

+
+
+
+
+
+ +
+
+
+ Regole Fanta +
+
+
+ I partecipanti al Fanta di RaceForFederica dovranno indovinare, o avvicinarsi il più possibile, l'ordine di arrivo di tutti i gran premi del campionato, il giro veloce, chi dei piloti farà DNF (aka did not finish) e il miglior costruttore (somma punti piloti). +
+ I punteggi dei singoli voti sono così strutturati: +
    +
  • Il punteggio dell'ordine di arrivo dipende dalla distanza tra il tuo voto e la posizione reale del pilota: +
      +
    • Posizione corretta = 7 PT
    • +
    • Posizione ±1 = 4 PT
    • +
    • Posizione ±2 = 2 PT
    • +
    • Oltre = 0 PT
    • +
    +
  • +
  • Giro veloce corretto: 5 PT
  • +
  • DNF corretto: 5 PT
  • +
  • Miglior costruttore corretto: 5 PT
  • +
+ Vota i prossimi 4 GP (ci sono 2 GP a settimana, ogni lunedì), e gioca con moderazione! +
+
+
+
+
+
\ No newline at end of file diff --git a/client/src/app/views/regole/regole.component.spec.ts b/client/src/app/views/regole/regole.component.spec.ts new file mode 100644 index 000000000..33bffccd2 --- /dev/null +++ b/client/src/app/views/regole/regole.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; + +import { RegoleComponent } from './regole.component'; + +describe('RegoleComponent', () => { + let component: RegoleComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [provideNoopAnimations(), ], + imports: [RegoleComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(RegoleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/views/regole/regole.component.ts b/client/src/app/views/regole/regole.component.ts new file mode 100644 index 000000000..39645b8b2 --- /dev/null +++ b/client/src/app/views/regole/regole.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-regole', + imports: [], + templateUrl: './regole.component.html' +}) +export class RegoleComponent { + +} diff --git a/client/src/app/views/regole/routes.ts b/client/src/app/views/regole/routes.ts new file mode 100644 index 000000000..2a2b65b4e --- /dev/null +++ b/client/src/app/views/regole/routes.ts @@ -0,0 +1,12 @@ +import { Routes } from '@angular/router'; + +export const routes: Routes = [ + { + path: '', + loadComponent: () => import('./regole.component').then(m => m.RegoleComponent), + data: { + title: $localize`Regole` + } + } +]; + diff --git a/src/assets/.gitkeep b/client/src/assets/.gitkeep similarity index 100% rename from src/assets/.gitkeep rename to client/src/assets/.gitkeep diff --git a/src/assets/angular.ico b/client/src/assets/angular.ico similarity index 100% rename from src/assets/angular.ico rename to client/src/assets/angular.ico diff --git a/src/assets/brand/coreui-angular-white.svg b/client/src/assets/brand/coreui-angular-white.svg similarity index 100% rename from src/assets/brand/coreui-angular-white.svg rename to client/src/assets/brand/coreui-angular-white.svg diff --git a/src/assets/brand/coreui-angular.svg b/client/src/assets/brand/coreui-angular.svg similarity index 100% rename from src/assets/brand/coreui-angular.svg rename to client/src/assets/brand/coreui-angular.svg diff --git a/src/assets/brand/coreui-base-white.svg b/client/src/assets/brand/coreui-base-white.svg similarity index 100% rename from src/assets/brand/coreui-base-white.svg rename to client/src/assets/brand/coreui-base-white.svg diff --git a/src/assets/brand/coreui-base.svg b/client/src/assets/brand/coreui-base.svg similarity index 100% rename from src/assets/brand/coreui-base.svg rename to client/src/assets/brand/coreui-base.svg diff --git a/src/assets/brand/coreui-signet-white.svg b/client/src/assets/brand/coreui-signet-white.svg similarity index 100% rename from src/assets/brand/coreui-signet-white.svg rename to client/src/assets/brand/coreui-signet-white.svg diff --git a/src/assets/brand/coreui-signet.svg b/client/src/assets/brand/coreui-signet.svg similarity index 100% rename from src/assets/brand/coreui-signet.svg rename to client/src/assets/brand/coreui-signet.svg diff --git a/src/assets/favicon.ico b/client/src/assets/favicon.ico similarity index 100% rename from src/assets/favicon.ico rename to client/src/assets/favicon.ico diff --git a/client/src/assets/images/TAG_Heuer_logo.png b/client/src/assets/images/TAG_Heuer_logo.png new file mode 100644 index 000000000..04cf69b2f Binary files /dev/null and b/client/src/assets/images/TAG_Heuer_logo.png differ diff --git a/client/src/assets/images/albo d'oro/sfondo.png b/client/src/assets/images/albo d'oro/sfondo.png new file mode 100644 index 000000000..2f7c68c3a Binary files /dev/null and b/client/src/assets/images/albo d'oro/sfondo.png differ diff --git a/client/src/assets/images/albo d'oro/texture.png b/client/src/assets/images/albo d'oro/texture.png new file mode 100644 index 000000000..795698a9c Binary files /dev/null and b/client/src/assets/images/albo d'oro/texture.png differ diff --git a/src/assets/images/angular.jpg b/client/src/assets/images/angular.jpg similarity index 100% rename from src/assets/images/angular.jpg rename to client/src/assets/images/angular.jpg diff --git a/client/src/assets/images/avatars/Dreandos.png b/client/src/assets/images/avatars/Dreandos.png new file mode 100644 index 000000000..b62e0b9cc Binary files /dev/null and b/client/src/assets/images/avatars/Dreandos.png differ diff --git a/client/src/assets/images/avatars/FASTman.png b/client/src/assets/images/avatars/FASTman.png new file mode 100644 index 000000000..51c68780c Binary files /dev/null and b/client/src/assets/images/avatars/FASTman.png differ diff --git a/client/src/assets/images/avatars/GiannisCorbe.png b/client/src/assets/images/avatars/GiannisCorbe.png new file mode 100644 index 000000000..8224c7e21 Binary files /dev/null and b/client/src/assets/images/avatars/GiannisCorbe.png differ diff --git a/client/src/assets/images/avatars/JJKudos.png b/client/src/assets/images/avatars/JJKudos.png new file mode 100644 index 000000000..4996c2dae Binary files /dev/null and b/client/src/assets/images/avatars/JJKudos.png differ diff --git a/client/src/assets/images/avatars/Lil Mvrck.png b/client/src/assets/images/avatars/Lil Mvrck.png new file mode 100644 index 000000000..55c1e15e1 Binary files /dev/null and b/client/src/assets/images/avatars/Lil Mvrck.png differ diff --git a/client/src/assets/images/avatars/Lil Mvrck1.png b/client/src/assets/images/avatars/Lil Mvrck1.png new file mode 100644 index 000000000..983215914 Binary files /dev/null and b/client/src/assets/images/avatars/Lil Mvrck1.png differ diff --git a/client/src/assets/images/avatars/Marcogang96.png b/client/src/assets/images/avatars/Marcogang96.png new file mode 100644 index 000000000..3f4786a23 Binary files /dev/null and b/client/src/assets/images/avatars/Marcogang96.png differ diff --git a/client/src/assets/images/avatars/OLD/FASTman.jpg b/client/src/assets/images/avatars/OLD/FASTman.jpg new file mode 100644 index 000000000..40cd7624e Binary files /dev/null and b/client/src/assets/images/avatars/OLD/FASTman.jpg differ diff --git a/client/src/assets/images/avatars/OLD/FASTman1.png b/client/src/assets/images/avatars/OLD/FASTman1.png new file mode 100644 index 000000000..68b039156 Binary files /dev/null and b/client/src/assets/images/avatars/OLD/FASTman1.png differ diff --git a/client/src/assets/images/avatars/OLD/GiannisCorbe.jpg b/client/src/assets/images/avatars/OLD/GiannisCorbe.jpg new file mode 100644 index 000000000..85351a89d Binary files /dev/null and b/client/src/assets/images/avatars/OLD/GiannisCorbe.jpg differ diff --git a/client/src/assets/images/avatars/OLD/GiannisCorbe1.png b/client/src/assets/images/avatars/OLD/GiannisCorbe1.png new file mode 100644 index 000000000..0217df7b8 Binary files /dev/null and b/client/src/assets/images/avatars/OLD/GiannisCorbe1.png differ diff --git a/client/src/assets/images/avatars/OLD/HeavyButt.jpg b/client/src/assets/images/avatars/OLD/HeavyButt.jpg new file mode 100644 index 000000000..4a849a996 Binary files /dev/null and b/client/src/assets/images/avatars/OLD/HeavyButt.jpg differ diff --git a/client/src/assets/images/avatars/OLD/HeavyButt.png b/client/src/assets/images/avatars/OLD/HeavyButt.png new file mode 100644 index 000000000..c8ebee641 Binary files /dev/null and b/client/src/assets/images/avatars/OLD/HeavyButt.png differ diff --git a/client/src/assets/images/avatars/OLD/HeavyButt1 (2).png b/client/src/assets/images/avatars/OLD/HeavyButt1 (2).png new file mode 100644 index 000000000..cbc569493 Binary files /dev/null and b/client/src/assets/images/avatars/OLD/HeavyButt1 (2).png differ diff --git a/client/src/assets/images/avatars/OLD/HeavyButt1.png b/client/src/assets/images/avatars/OLD/HeavyButt1.png new file mode 100644 index 000000000..668a2a0cb Binary files /dev/null and b/client/src/assets/images/avatars/OLD/HeavyButt1.png differ diff --git a/client/src/assets/images/avatars/OLD/HeavyButt_old.png b/client/src/assets/images/avatars/OLD/HeavyButt_old.png new file mode 100644 index 000000000..b46f305eb Binary files /dev/null and b/client/src/assets/images/avatars/OLD/HeavyButt_old.png differ diff --git a/client/src/assets/images/avatars/OLD/JJKudos1.png b/client/src/assets/images/avatars/OLD/JJKudos1.png new file mode 100644 index 000000000..3e52138c0 Binary files /dev/null and b/client/src/assets/images/avatars/OLD/JJKudos1.png differ diff --git a/client/src/assets/images/avatars/OLD/Lil Mvrck (old).png b/client/src/assets/images/avatars/OLD/Lil Mvrck (old).png new file mode 100644 index 000000000..3b83eb925 Binary files /dev/null and b/client/src/assets/images/avatars/OLD/Lil Mvrck (old).png differ diff --git a/client/src/assets/images/avatars/OLD/Lil Mvrck1.png b/client/src/assets/images/avatars/OLD/Lil Mvrck1.png new file mode 100644 index 000000000..983215914 Binary files /dev/null and b/client/src/assets/images/avatars/OLD/Lil Mvrck1.png differ diff --git a/client/src/assets/images/avatars/OLD/Marcogang96.jpg b/client/src/assets/images/avatars/OLD/Marcogang96.jpg new file mode 100644 index 000000000..7255c3f5f Binary files /dev/null and b/client/src/assets/images/avatars/OLD/Marcogang96.jpg differ diff --git a/client/src/assets/images/avatars/OLD/Marcogang961.png b/client/src/assets/images/avatars/OLD/Marcogang961.png new file mode 100644 index 000000000..ab6ec6b65 Binary files /dev/null and b/client/src/assets/images/avatars/OLD/Marcogang961.png differ diff --git a/client/src/assets/images/avatars/OLD/Octimus101.png b/client/src/assets/images/avatars/OLD/Octimus101.png new file mode 100644 index 000000000..676d39f69 Binary files /dev/null and b/client/src/assets/images/avatars/OLD/Octimus101.png differ diff --git a/client/src/assets/images/avatars/OLD/redmamba_99_(old).png b/client/src/assets/images/avatars/OLD/redmamba_99_(old).png new file mode 100644 index 000000000..432e83aa6 Binary files /dev/null and b/client/src/assets/images/avatars/OLD/redmamba_99_(old).png differ diff --git a/client/src/assets/images/avatars/OLD/redmamba_99_.jpg b/client/src/assets/images/avatars/OLD/redmamba_99_.jpg new file mode 100644 index 000000000..55c5c09c2 Binary files /dev/null and b/client/src/assets/images/avatars/OLD/redmamba_99_.jpg differ diff --git a/client/src/assets/images/avatars/OLD/redmamba_99_1.png b/client/src/assets/images/avatars/OLD/redmamba_99_1.png new file mode 100644 index 000000000..6220486a8 Binary files /dev/null and b/client/src/assets/images/avatars/OLD/redmamba_99_1.png differ diff --git a/client/src/assets/images/avatars/Octimus10.png b/client/src/assets/images/avatars/Octimus10.png new file mode 100644 index 000000000..fb2b907c4 Binary files /dev/null and b/client/src/assets/images/avatars/Octimus10.png differ diff --git a/client/src/assets/images/avatars/redmamba_99_.png b/client/src/assets/images/avatars/redmamba_99_.png new file mode 100644 index 000000000..ffaa7c1d0 Binary files /dev/null and b/client/src/assets/images/avatars/redmamba_99_.png differ diff --git a/client/src/assets/images/avatars_fanta/1.png b/client/src/assets/images/avatars_fanta/1.png new file mode 100644 index 000000000..2ab0c069a Binary files /dev/null and b/client/src/assets/images/avatars_fanta/1.png differ diff --git a/client/src/assets/images/avatars_fanta/10.png b/client/src/assets/images/avatars_fanta/10.png new file mode 100644 index 000000000..a1710facc Binary files /dev/null and b/client/src/assets/images/avatars_fanta/10.png differ diff --git a/client/src/assets/images/avatars_fanta/11.png b/client/src/assets/images/avatars_fanta/11.png new file mode 100644 index 000000000..2c745df0e Binary files /dev/null and b/client/src/assets/images/avatars_fanta/11.png differ diff --git a/client/src/assets/images/avatars_fanta/12.png b/client/src/assets/images/avatars_fanta/12.png new file mode 100644 index 000000000..94de4b233 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/12.png differ diff --git a/client/src/assets/images/avatars_fanta/13.png b/client/src/assets/images/avatars_fanta/13.png new file mode 100644 index 000000000..0e27b9940 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/13.png differ diff --git a/client/src/assets/images/avatars_fanta/14.png b/client/src/assets/images/avatars_fanta/14.png new file mode 100644 index 000000000..5e998a71a Binary files /dev/null and b/client/src/assets/images/avatars_fanta/14.png differ diff --git a/client/src/assets/images/avatars_fanta/15.png b/client/src/assets/images/avatars_fanta/15.png new file mode 100644 index 000000000..5cd415aa7 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/15.png differ diff --git a/client/src/assets/images/avatars_fanta/16.png b/client/src/assets/images/avatars_fanta/16.png new file mode 100644 index 000000000..22319387d Binary files /dev/null and b/client/src/assets/images/avatars_fanta/16.png differ diff --git a/client/src/assets/images/avatars_fanta/17.png b/client/src/assets/images/avatars_fanta/17.png new file mode 100644 index 000000000..b7bd5090b Binary files /dev/null and b/client/src/assets/images/avatars_fanta/17.png differ diff --git a/client/src/assets/images/avatars_fanta/18.png b/client/src/assets/images/avatars_fanta/18.png new file mode 100644 index 000000000..01b4c920f Binary files /dev/null and b/client/src/assets/images/avatars_fanta/18.png differ diff --git a/client/src/assets/images/avatars_fanta/19.png b/client/src/assets/images/avatars_fanta/19.png new file mode 100644 index 000000000..93d7d122a Binary files /dev/null and b/client/src/assets/images/avatars_fanta/19.png differ diff --git a/client/src/assets/images/avatars_fanta/2.png b/client/src/assets/images/avatars_fanta/2.png new file mode 100644 index 000000000..205a9ffbe Binary files /dev/null and b/client/src/assets/images/avatars_fanta/2.png differ diff --git a/client/src/assets/images/avatars_fanta/20.png b/client/src/assets/images/avatars_fanta/20.png new file mode 100644 index 000000000..a0c73192a Binary files /dev/null and b/client/src/assets/images/avatars_fanta/20.png differ diff --git a/client/src/assets/images/avatars_fanta/21.png b/client/src/assets/images/avatars_fanta/21.png new file mode 100644 index 000000000..6b6638e1a Binary files /dev/null and b/client/src/assets/images/avatars_fanta/21.png differ diff --git a/client/src/assets/images/avatars_fanta/22.png b/client/src/assets/images/avatars_fanta/22.png new file mode 100644 index 000000000..fc8a894cc Binary files /dev/null and b/client/src/assets/images/avatars_fanta/22.png differ diff --git a/client/src/assets/images/avatars_fanta/23.png b/client/src/assets/images/avatars_fanta/23.png new file mode 100644 index 000000000..1152b5f7d Binary files /dev/null and b/client/src/assets/images/avatars_fanta/23.png differ diff --git a/client/src/assets/images/avatars_fanta/24.png b/client/src/assets/images/avatars_fanta/24.png new file mode 100644 index 000000000..8597f98ca Binary files /dev/null and b/client/src/assets/images/avatars_fanta/24.png differ diff --git a/client/src/assets/images/avatars_fanta/25.png b/client/src/assets/images/avatars_fanta/25.png new file mode 100644 index 000000000..08b26752b Binary files /dev/null and b/client/src/assets/images/avatars_fanta/25.png differ diff --git a/client/src/assets/images/avatars_fanta/27.png b/client/src/assets/images/avatars_fanta/27.png new file mode 100644 index 000000000..e9d4e7a4a Binary files /dev/null and b/client/src/assets/images/avatars_fanta/27.png differ diff --git a/client/src/assets/images/avatars_fanta/28.png b/client/src/assets/images/avatars_fanta/28.png new file mode 100644 index 000000000..94de4b233 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/28.png differ diff --git a/client/src/assets/images/avatars_fanta/29.png b/client/src/assets/images/avatars_fanta/29.png new file mode 100644 index 000000000..2ab0c069a Binary files /dev/null and b/client/src/assets/images/avatars_fanta/29.png differ diff --git a/client/src/assets/images/avatars_fanta/3.png b/client/src/assets/images/avatars_fanta/3.png new file mode 100644 index 000000000..6e421b520 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/3.png differ diff --git a/client/src/assets/images/avatars_fanta/30.png b/client/src/assets/images/avatars_fanta/30.png new file mode 100644 index 000000000..9955cc9cb Binary files /dev/null and b/client/src/assets/images/avatars_fanta/30.png differ diff --git a/client/src/assets/images/avatars_fanta/31.png b/client/src/assets/images/avatars_fanta/31.png new file mode 100644 index 000000000..6e421b520 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/31.png differ diff --git a/client/src/assets/images/avatars_fanta/32.png b/client/src/assets/images/avatars_fanta/32.png new file mode 100644 index 000000000..47e536dd0 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/32.png differ diff --git a/client/src/assets/images/avatars_fanta/33.png b/client/src/assets/images/avatars_fanta/33.png new file mode 100644 index 000000000..376e18c1f Binary files /dev/null and b/client/src/assets/images/avatars_fanta/33.png differ diff --git a/client/src/assets/images/avatars_fanta/34.png b/client/src/assets/images/avatars_fanta/34.png new file mode 100644 index 000000000..c2003c808 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/34.png differ diff --git a/client/src/assets/images/avatars_fanta/35.png b/client/src/assets/images/avatars_fanta/35.png new file mode 100644 index 000000000..0d36adb84 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/35.png differ diff --git a/client/src/assets/images/avatars_fanta/36.png b/client/src/assets/images/avatars_fanta/36.png new file mode 100644 index 000000000..4ff4c9f54 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/36.png differ diff --git a/client/src/assets/images/avatars_fanta/37.png b/client/src/assets/images/avatars_fanta/37.png new file mode 100644 index 000000000..22319387d Binary files /dev/null and b/client/src/assets/images/avatars_fanta/37.png differ diff --git a/client/src/assets/images/avatars_fanta/38.png b/client/src/assets/images/avatars_fanta/38.png new file mode 100644 index 000000000..029302c07 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/38.png differ diff --git a/client/src/assets/images/avatars_fanta/39.png b/client/src/assets/images/avatars_fanta/39.png new file mode 100644 index 000000000..a1710facc Binary files /dev/null and b/client/src/assets/images/avatars_fanta/39.png differ diff --git a/client/src/assets/images/avatars_fanta/4.png b/client/src/assets/images/avatars_fanta/4.png new file mode 100644 index 000000000..b456831f7 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/4.png differ diff --git a/client/src/assets/images/avatars_fanta/40.png b/client/src/assets/images/avatars_fanta/40.png new file mode 100644 index 000000000..2c745df0e Binary files /dev/null and b/client/src/assets/images/avatars_fanta/40.png differ diff --git a/client/src/assets/images/avatars_fanta/41.png b/client/src/assets/images/avatars_fanta/41.png new file mode 100644 index 000000000..a4450af1c Binary files /dev/null and b/client/src/assets/images/avatars_fanta/41.png differ diff --git a/client/src/assets/images/avatars_fanta/5.png b/client/src/assets/images/avatars_fanta/5.png new file mode 100644 index 000000000..a3cf9eae3 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/5.png differ diff --git a/client/src/assets/images/avatars_fanta/6.png b/client/src/assets/images/avatars_fanta/6.png new file mode 100644 index 000000000..c2003c808 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/6.png differ diff --git a/client/src/assets/images/avatars_fanta/7.png b/client/src/assets/images/avatars_fanta/7.png new file mode 100644 index 000000000..d970297da Binary files /dev/null and b/client/src/assets/images/avatars_fanta/7.png differ diff --git a/client/src/assets/images/avatars_fanta/8.png b/client/src/assets/images/avatars_fanta/8.png new file mode 100644 index 000000000..4ff4c9f54 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/8.png differ diff --git a/client/src/assets/images/avatars_fanta/9.png b/client/src/assets/images/avatars_fanta/9.png new file mode 100644 index 000000000..26f418377 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/9.png differ diff --git a/client/src/assets/images/avatars_fanta/BoxBoxBuddy.png b/client/src/assets/images/avatars_fanta/BoxBoxBuddy.png new file mode 100644 index 000000000..74ebbfbc8 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/BoxBoxBuddy.png differ diff --git a/client/src/assets/images/avatars_fanta/IMG-20241210-WA0015 - Gaia Franzin.jpg b/client/src/assets/images/avatars_fanta/IMG-20241210-WA0015 - Gaia Franzin.jpg new file mode 100644 index 000000000..39a60d472 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/IMG-20241210-WA0015 - Gaia Franzin.jpg differ diff --git a/client/src/assets/images/avatars_fanta/chichi.jpg b/client/src/assets/images/avatars_fanta/chichi.jpg new file mode 100644 index 000000000..44f37b57e Binary files /dev/null and b/client/src/assets/images/avatars_fanta/chichi.jpg differ diff --git a/client/src/assets/images/avatars_fanta/dd14.png b/client/src/assets/images/avatars_fanta/dd14.png new file mode 100644 index 000000000..196ebd0e8 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/dd14.png differ diff --git a/client/src/assets/images/avatars_fanta/dd5.png b/client/src/assets/images/avatars_fanta/dd5.png new file mode 100644 index 000000000..376e18c1f Binary files /dev/null and b/client/src/assets/images/avatars_fanta/dd5.png differ diff --git a/client/src/assets/images/avatars_fanta/dd7.png b/client/src/assets/images/avatars_fanta/dd7.png new file mode 100644 index 000000000..0d36adb84 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/dd7.png differ diff --git a/client/src/assets/images/avatars_fanta/default.png b/client/src/assets/images/avatars_fanta/default.png new file mode 100644 index 000000000..0c8bba834 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/default.png differ diff --git a/client/src/assets/images/avatars_fanta/propriogiotto.png b/client/src/assets/images/avatars_fanta/propriogiotto.png new file mode 100644 index 000000000..e8c81f385 Binary files /dev/null and b/client/src/assets/images/avatars_fanta/propriogiotto.png differ diff --git a/client/src/assets/images/championshipResoult/classifica2025.jpg b/client/src/assets/images/championshipResoult/classifica2025.jpg new file mode 100644 index 000000000..fb85e8a22 Binary files /dev/null and b/client/src/assets/images/championshipResoult/classifica2025.jpg differ diff --git a/client/src/assets/images/constructors/Alpine.svg b/client/src/assets/images/constructors/Alpine.svg new file mode 100644 index 000000000..e4abe4023 --- /dev/null +++ b/client/src/assets/images/constructors/Alpine.svg @@ -0,0 +1 @@ +® \ No newline at end of file diff --git a/client/src/assets/images/constructors/AstonMartin.svg b/client/src/assets/images/constructors/AstonMartin.svg new file mode 100644 index 000000000..705e03743 --- /dev/null +++ b/client/src/assets/images/constructors/AstonMartin.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/images/constructors/Ferrari.svg b/client/src/assets/images/constructors/Ferrari.svg new file mode 100644 index 000000000..37b4b7db9 --- /dev/null +++ b/client/src/assets/images/constructors/Ferrari.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/images/constructors/Haas.svg b/client/src/assets/images/constructors/Haas.svg new file mode 100644 index 000000000..97c855573 --- /dev/null +++ b/client/src/assets/images/constructors/Haas.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/src/assets/images/constructors/MaFIA Racing Team.png b/client/src/assets/images/constructors/MaFIA Racing Team.png new file mode 100644 index 000000000..e0864067d Binary files /dev/null and b/client/src/assets/images/constructors/MaFIA Racing Team.png differ diff --git a/client/src/assets/images/constructors/Mercedes.svg b/client/src/assets/images/constructors/Mercedes.svg new file mode 100644 index 000000000..f18629007 --- /dev/null +++ b/client/src/assets/images/constructors/Mercedes.svg @@ -0,0 +1,2 @@ + +Mercedes icon \ No newline at end of file diff --git a/client/src/assets/images/constructors/Panda Racing Team.png b/client/src/assets/images/constructors/Panda Racing Team.png new file mode 100644 index 000000000..9b8ca72fa Binary files /dev/null and b/client/src/assets/images/constructors/Panda Racing Team.png differ diff --git a/client/src/assets/images/constructors/ParkSide Racing Team.png b/client/src/assets/images/constructors/ParkSide Racing Team.png new file mode 100644 index 000000000..7f7ab2a74 Binary files /dev/null and b/client/src/assets/images/constructors/ParkSide Racing Team.png differ diff --git a/client/src/assets/images/constructors/Racing Bulls.svg b/client/src/assets/images/constructors/Racing Bulls.svg new file mode 100644 index 000000000..1488afde3 --- /dev/null +++ b/client/src/assets/images/constructors/Racing Bulls.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/assets/images/constructors/Redbull.svg b/client/src/assets/images/constructors/Redbull.svg new file mode 100644 index 000000000..d1fa0a6a3 --- /dev/null +++ b/client/src/assets/images/constructors/Redbull.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/client/src/assets/images/constructors/TURKX Racing Team.png b/client/src/assets/images/constructors/TURKX Racing Team.png new file mode 100644 index 000000000..683f73236 Binary files /dev/null and b/client/src/assets/images/constructors/TURKX Racing Team.png differ diff --git a/client/src/assets/images/constructors/Williams.svg b/client/src/assets/images/constructors/Williams.svg new file mode 100644 index 000000000..f4df2b4df --- /dev/null +++ b/client/src/assets/images/constructors/Williams.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/src/assets/images/fake fanta 1.png b/client/src/assets/images/fake fanta 1.png new file mode 100644 index 000000000..9e0825471 Binary files /dev/null and b/client/src/assets/images/fake fanta 1.png differ diff --git a/client/src/assets/images/fanta_players/0.jpg b/client/src/assets/images/fanta_players/0.jpg new file mode 100644 index 000000000..55c5c09c2 Binary files /dev/null and b/client/src/assets/images/fanta_players/0.jpg differ diff --git a/client/src/assets/images/fanta_players/1.jpg b/client/src/assets/images/fanta_players/1.jpg new file mode 100644 index 000000000..40cd7624e Binary files /dev/null and b/client/src/assets/images/fanta_players/1.jpg differ diff --git a/client/src/assets/images/fanta_players/2.jpg b/client/src/assets/images/fanta_players/2.jpg new file mode 100644 index 000000000..85351a89d Binary files /dev/null and b/client/src/assets/images/fanta_players/2.jpg differ diff --git a/client/src/assets/images/fanta_players/3.jpg b/client/src/assets/images/fanta_players/3.jpg new file mode 100644 index 000000000..4a849a996 Binary files /dev/null and b/client/src/assets/images/fanta_players/3.jpg differ diff --git a/client/src/assets/images/fanta_players/4.jpg b/client/src/assets/images/fanta_players/4.jpg new file mode 100644 index 000000000..7255c3f5f Binary files /dev/null and b/client/src/assets/images/fanta_players/4.jpg differ diff --git a/client/src/assets/images/logo_raceforfederica.png b/client/src/assets/images/logo_raceforfederica.png new file mode 100644 index 000000000..26f418377 Binary files /dev/null and b/client/src/assets/images/logo_raceforfederica.png differ diff --git a/client/src/assets/images/logo_raceforfederica.svg b/client/src/assets/images/logo_raceforfederica.svg new file mode 100644 index 000000000..633699f79 --- /dev/null +++ b/client/src/assets/images/logo_raceforfederica.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/client/src/assets/images/others/github_logo.png b/client/src/assets/images/others/github_logo.png new file mode 100644 index 000000000..50b817522 Binary files /dev/null and b/client/src/assets/images/others/github_logo.png differ diff --git a/client/src/assets/images/others/satispay_logo.png b/client/src/assets/images/others/satispay_logo.png new file mode 100644 index 000000000..9e1e8d6b2 Binary files /dev/null and b/client/src/assets/images/others/satispay_logo.png differ diff --git a/src/assets/images/react.jpg b/client/src/assets/images/react.jpg similarity index 100% rename from src/assets/images/react.jpg rename to client/src/assets/images/react.jpg diff --git a/client/src/assets/images/readme/campionato.png b/client/src/assets/images/readme/campionato.png new file mode 100644 index 000000000..f4905bdd8 Binary files /dev/null and b/client/src/assets/images/readme/campionato.png differ diff --git a/client/src/assets/images/readme/dashboard.png b/client/src/assets/images/readme/dashboard.png new file mode 100644 index 000000000..beb244789 Binary files /dev/null and b/client/src/assets/images/readme/dashboard.png differ diff --git a/client/src/assets/images/readme/f1_evolution.gif b/client/src/assets/images/readme/f1_evolution.gif new file mode 100644 index 000000000..28162ba32 Binary files /dev/null and b/client/src/assets/images/readme/f1_evolution.gif differ diff --git a/client/src/assets/images/readme/fanta.png b/client/src/assets/images/readme/fanta.png new file mode 100644 index 000000000..d464ec959 Binary files /dev/null and b/client/src/assets/images/readme/fanta.png differ diff --git a/client/src/assets/images/readme/fanta_risultato.png b/client/src/assets/images/readme/fanta_risultato.png new file mode 100644 index 000000000..ae6eac6b6 Binary files /dev/null and b/client/src/assets/images/readme/fanta_risultato.png differ diff --git a/client/src/assets/images/readme/fanta_voto.png b/client/src/assets/images/readme/fanta_voto.png new file mode 100644 index 000000000..2831e5f72 Binary files /dev/null and b/client/src/assets/images/readme/fanta_voto.png differ diff --git a/client/src/assets/images/readme/piloti.png b/client/src/assets/images/readme/piloti.png new file mode 100644 index 000000000..c0fb094da Binary files /dev/null and b/client/src/assets/images/readme/piloti.png differ diff --git a/client/src/assets/images/rolex_logo.png b/client/src/assets/images/rolex_logo.png new file mode 100644 index 000000000..15e610c5c Binary files /dev/null and b/client/src/assets/images/rolex_logo.png differ diff --git a/client/src/assets/images/tracks/1.png b/client/src/assets/images/tracks/1.png new file mode 100644 index 000000000..6ea66aafd Binary files /dev/null and b/client/src/assets/images/tracks/1.png differ diff --git a/client/src/assets/images/tracks/10.png b/client/src/assets/images/tracks/10.png new file mode 100644 index 000000000..fb5b2b66d Binary files /dev/null and b/client/src/assets/images/tracks/10.png differ diff --git a/client/src/assets/images/tracks/11.png b/client/src/assets/images/tracks/11.png new file mode 100644 index 000000000..c105580e1 Binary files /dev/null and b/client/src/assets/images/tracks/11.png differ diff --git a/client/src/assets/images/tracks/12.png b/client/src/assets/images/tracks/12.png new file mode 100644 index 000000000..82338a331 Binary files /dev/null and b/client/src/assets/images/tracks/12.png differ diff --git a/client/src/assets/images/tracks/13.png b/client/src/assets/images/tracks/13.png new file mode 100644 index 000000000..5afdbed52 Binary files /dev/null and b/client/src/assets/images/tracks/13.png differ diff --git a/client/src/assets/images/tracks/14.png b/client/src/assets/images/tracks/14.png new file mode 100644 index 000000000..f46b03394 Binary files /dev/null and b/client/src/assets/images/tracks/14.png differ diff --git a/client/src/assets/images/tracks/15.png b/client/src/assets/images/tracks/15.png new file mode 100644 index 000000000..a3db55aac Binary files /dev/null and b/client/src/assets/images/tracks/15.png differ diff --git a/client/src/assets/images/tracks/16.png b/client/src/assets/images/tracks/16.png new file mode 100644 index 000000000..a3001d38e Binary files /dev/null and b/client/src/assets/images/tracks/16.png differ diff --git a/client/src/assets/images/tracks/17.png b/client/src/assets/images/tracks/17.png new file mode 100644 index 000000000..602cedac2 Binary files /dev/null and b/client/src/assets/images/tracks/17.png differ diff --git a/client/src/assets/images/tracks/18.png b/client/src/assets/images/tracks/18.png new file mode 100644 index 000000000..73f5c047e Binary files /dev/null and b/client/src/assets/images/tracks/18.png differ diff --git a/client/src/assets/images/tracks/19.png b/client/src/assets/images/tracks/19.png new file mode 100644 index 000000000..fffc97397 Binary files /dev/null and b/client/src/assets/images/tracks/19.png differ diff --git a/client/src/assets/images/tracks/2.png b/client/src/assets/images/tracks/2.png new file mode 100644 index 000000000..89aeac142 Binary files /dev/null and b/client/src/assets/images/tracks/2.png differ diff --git a/client/src/assets/images/tracks/20.png b/client/src/assets/images/tracks/20.png new file mode 100644 index 000000000..041c96da8 Binary files /dev/null and b/client/src/assets/images/tracks/20.png differ diff --git a/client/src/assets/images/tracks/21.png b/client/src/assets/images/tracks/21.png new file mode 100644 index 000000000..16c10d885 Binary files /dev/null and b/client/src/assets/images/tracks/21.png differ diff --git a/client/src/assets/images/tracks/22.png b/client/src/assets/images/tracks/22.png new file mode 100644 index 000000000..583d9c5b0 Binary files /dev/null and b/client/src/assets/images/tracks/22.png differ diff --git a/client/src/assets/images/tracks/23.png b/client/src/assets/images/tracks/23.png new file mode 100644 index 000000000..d1b84e493 Binary files /dev/null and b/client/src/assets/images/tracks/23.png differ diff --git a/client/src/assets/images/tracks/24.png b/client/src/assets/images/tracks/24.png new file mode 100644 index 000000000..7fc42d5d9 Binary files /dev/null and b/client/src/assets/images/tracks/24.png differ diff --git a/client/src/assets/images/tracks/25.png b/client/src/assets/images/tracks/25.png new file mode 100644 index 000000000..47aecbb92 Binary files /dev/null and b/client/src/assets/images/tracks/25.png differ diff --git a/client/src/assets/images/tracks/3.png b/client/src/assets/images/tracks/3.png new file mode 100644 index 000000000..e0ed5f459 Binary files /dev/null and b/client/src/assets/images/tracks/3.png differ diff --git a/client/src/assets/images/tracks/4.png b/client/src/assets/images/tracks/4.png new file mode 100644 index 000000000..47e9eab65 Binary files /dev/null and b/client/src/assets/images/tracks/4.png differ diff --git a/client/src/assets/images/tracks/5.png b/client/src/assets/images/tracks/5.png new file mode 100644 index 000000000..deccf5d4b Binary files /dev/null and b/client/src/assets/images/tracks/5.png differ diff --git a/client/src/assets/images/tracks/6.png b/client/src/assets/images/tracks/6.png new file mode 100644 index 000000000..24843ce4b Binary files /dev/null and b/client/src/assets/images/tracks/6.png differ diff --git a/client/src/assets/images/tracks/7.png b/client/src/assets/images/tracks/7.png new file mode 100644 index 000000000..55f254f09 Binary files /dev/null and b/client/src/assets/images/tracks/7.png differ diff --git a/client/src/assets/images/tracks/8.png b/client/src/assets/images/tracks/8.png new file mode 100644 index 000000000..26f745f09 Binary files /dev/null and b/client/src/assets/images/tracks/8.png differ diff --git a/client/src/assets/images/tracks/9.png b/client/src/assets/images/tracks/9.png new file mode 100644 index 000000000..8cf10cab5 Binary files /dev/null and b/client/src/assets/images/tracks/9.png differ diff --git a/client/src/assets/images/under_construction_fanta.png b/client/src/assets/images/under_construction_fanta.png new file mode 100644 index 000000000..c486146f6 Binary files /dev/null and b/client/src/assets/images/under_construction_fanta.png differ diff --git a/src/assets/images/vue.jpg b/client/src/assets/images/vue.jpg similarity index 100% rename from src/assets/images/vue.jpg rename to client/src/assets/images/vue.jpg diff --git a/client/src/assets/medals/medal_first.png b/client/src/assets/medals/medal_first.png new file mode 100644 index 000000000..a46c7b90d Binary files /dev/null and b/client/src/assets/medals/medal_first.png differ diff --git a/client/src/assets/medals/medal_first.svg b/client/src/assets/medals/medal_first.svg new file mode 100644 index 000000000..df6d2d981 --- /dev/null +++ b/client/src/assets/medals/medal_first.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/client/src/assets/medals/medal_second.png b/client/src/assets/medals/medal_second.png new file mode 100644 index 000000000..7a35f9fd1 Binary files /dev/null and b/client/src/assets/medals/medal_second.png differ diff --git a/client/src/assets/medals/medal_second.svg b/client/src/assets/medals/medal_second.svg new file mode 100644 index 000000000..a2436cb70 --- /dev/null +++ b/client/src/assets/medals/medal_second.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/client/src/assets/medals/medal_third.png b/client/src/assets/medals/medal_third.png new file mode 100644 index 000000000..b532376f7 Binary files /dev/null and b/client/src/assets/medals/medal_third.png differ diff --git a/client/src/assets/medals/medal_third.svg b/client/src/assets/medals/medal_third.svg new file mode 100644 index 000000000..2fc31c849 --- /dev/null +++ b/client/src/assets/medals/medal_third.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/components/docs-callout/docs-callout.component.html b/client/src/components/docs-callout/docs-callout.component.html similarity index 100% rename from src/components/docs-callout/docs-callout.component.html rename to client/src/components/docs-callout/docs-callout.component.html diff --git a/src/components/docs-callout/docs-callout.component.scss b/client/src/components/docs-callout/docs-callout.component.scss similarity index 100% rename from src/components/docs-callout/docs-callout.component.scss rename to client/src/components/docs-callout/docs-callout.component.scss diff --git a/src/components/docs-callout/docs-callout.component.spec.ts b/client/src/components/docs-callout/docs-callout.component.spec.ts similarity index 85% rename from src/components/docs-callout/docs-callout.component.spec.ts rename to client/src/components/docs-callout/docs-callout.component.spec.ts index 9bdd98162..c6ee79afb 100644 --- a/src/components/docs-callout/docs-callout.component.spec.ts +++ b/client/src/components/docs-callout/docs-callout.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { DocsCalloutComponent } from './docs-callout.component'; import { CalloutModule } from '@coreui/angular'; @@ -9,6 +10,7 @@ describe('DocsCalloutComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ + providers: [provideNoopAnimations(), ], imports: [CalloutModule, DocsCalloutComponent] }) .compileComponents(); diff --git a/src/components/docs-callout/docs-callout.component.ts b/client/src/components/docs-callout/docs-callout.component.ts similarity index 70% rename from src/components/docs-callout/docs-callout.component.ts rename to client/src/components/docs-callout/docs-callout.component.ts index e0445e434..bf6a1f767 100644 --- a/src/components/docs-callout/docs-callout.component.ts +++ b/client/src/components/docs-callout/docs-callout.component.ts @@ -4,19 +4,16 @@ import { NgTemplateOutlet } from '@angular/common'; import { CalloutComponent } from '@coreui/angular'; @Component({ - selector: 'app-docs-callout', - templateUrl: './docs-callout.component.html', - styleUrls: ['./docs-callout.component.scss'], - standalone: true, - imports: [CalloutComponent, NgTemplateOutlet] + selector: 'app-docs-callout', + templateUrl: './docs-callout.component.html', + styleUrls: ['./docs-callout.component.scss'], + imports: [CalloutComponent, NgTemplateOutlet] }) export class DocsCalloutComponent { - @Input() name: string = ''; + @Input() name = ''; - constructor() { } - - private _href: string = 'https://coreui.io/angular/docs/'; + private _href = 'https://coreui.io/angular/docs/'; get href(): string { return this._href; diff --git a/src/components/docs-example/docs-example.component.html b/client/src/components/docs-example/docs-example.component.html similarity index 100% rename from src/components/docs-example/docs-example.component.html rename to client/src/components/docs-example/docs-example.component.html diff --git a/src/components/docs-example/docs-example.component.scss b/client/src/components/docs-example/docs-example.component.scss similarity index 100% rename from src/components/docs-example/docs-example.component.scss rename to client/src/components/docs-example/docs-example.component.scss diff --git a/src/components/docs-example/docs-example.component.spec.ts b/client/src/components/docs-example/docs-example.component.spec.ts similarity index 93% rename from src/components/docs-example/docs-example.component.spec.ts rename to client/src/components/docs-example/docs-example.component.spec.ts index 45996fc87..8ad5c7318 100644 --- a/src/components/docs-example/docs-example.component.spec.ts +++ b/client/src/components/docs-example/docs-example.component.spec.ts @@ -8,6 +8,7 @@ // // beforeEach(async () => { // await TestBed.configureTestingModule({ +// providers: [provideNoopAnimations()], // declarations: [ DocsExampleComponent ] // }) // .compileComponents(); diff --git a/src/components/docs-example/docs-example.component.ts b/client/src/components/docs-example/docs-example.component.ts similarity index 98% rename from src/components/docs-example/docs-example.component.ts rename to client/src/components/docs-example/docs-example.component.ts index f6d26a407..8949d5383 100644 --- a/src/components/docs-example/docs-example.component.ts +++ b/client/src/components/docs-example/docs-example.component.ts @@ -18,7 +18,6 @@ import { NavComponent, NavItemComponent, NavLinkDirective } from '@coreui/angula templateUrl: './docs-example.component.html', styleUrls: ['./docs-example.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, imports: [NavComponent, NavItemComponent, NavLinkDirective, RouterLink, IconDirective] }) export class DocsExampleComponent implements AfterContentInit, AfterViewInit { diff --git a/src/components/docs-link/docs-link.component.html b/client/src/components/docs-link/docs-link.component.html similarity index 100% rename from src/components/docs-link/docs-link.component.html rename to client/src/components/docs-link/docs-link.component.html diff --git a/src/components/docs-link/docs-link.component.scss b/client/src/components/docs-link/docs-link.component.scss similarity index 100% rename from src/components/docs-link/docs-link.component.scss rename to client/src/components/docs-link/docs-link.component.scss diff --git a/src/components/docs-link/docs-link.component.spec.ts b/client/src/components/docs-link/docs-link.component.spec.ts similarity index 83% rename from src/components/docs-link/docs-link.component.spec.ts rename to client/src/components/docs-link/docs-link.component.spec.ts index 207ea27dd..d2d01b260 100644 --- a/src/components/docs-link/docs-link.component.spec.ts +++ b/client/src/components/docs-link/docs-link.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { DocsLinkComponent } from './docs-link.component'; @@ -8,6 +9,7 @@ describe('DocsLinkComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ + providers: [provideNoopAnimations(), ], imports: [DocsLinkComponent] }) .compileComponents(); diff --git a/src/components/docs-link/docs-link.component.ts b/client/src/components/docs-link/docs-link.component.ts similarity index 89% rename from src/components/docs-link/docs-link.component.ts rename to client/src/components/docs-link/docs-link.component.ts index 0831925ea..40c7315da 100644 --- a/src/components/docs-link/docs-link.component.ts +++ b/client/src/components/docs-link/docs-link.component.ts @@ -3,8 +3,7 @@ import { Component, HostBinding, Input, OnInit } from '@angular/core'; @Component({ selector: 'app-docs-link', templateUrl: './docs-link.component.html', - styleUrls: ['./docs-link.component.scss'], - standalone: true + styleUrls: ['./docs-link.component.scss'] }) export class DocsLinkComponent implements OnInit { diff --git a/src/components/index.ts b/client/src/components/index.ts similarity index 100% rename from src/components/index.ts rename to client/src/components/index.ts diff --git a/client/src/components/link-box/link-box.component.html b/client/src/components/link-box/link-box.component.html new file mode 100644 index 000000000..323c329df --- /dev/null +++ b/client/src/components/link-box/link-box.component.html @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/client/src/components/link-box/link-box.component.scss b/client/src/components/link-box/link-box.component.scss new file mode 100644 index 000000000..3f868af59 --- /dev/null +++ b/client/src/components/link-box/link-box.component.scss @@ -0,0 +1,34 @@ +.transparent-button { + background: transparent !important; + border: none !important; + padding: 0.5rem 1rem; + margin: 0; + transition: all 0.3s ease; + color: var(--cui-nav-link-color, #8a93a2) !important; + + &:hover { + background: transparent !important; + color: var(--hover-color) !important; + + .social-icon { + color: var(--hover-color) !important; + } + } + + .social-icon { + width: 1.2rem; + height: 1.2rem; + transition: color 0.3s ease; + color: var(--cui-nav-link-color, #8a93a2); + } +} + +.link-name { + display: none; +} + +@media (min-width: 1000px) { + .link-name { + display: inline; + } +} \ No newline at end of file diff --git a/client/src/components/link-box/link-box.component.spec.ts b/client/src/components/link-box/link-box.component.spec.ts new file mode 100644 index 000000000..f50e3e5fb --- /dev/null +++ b/client/src/components/link-box/link-box.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { LinkBoxComponent } from './link-box.component'; + +describe('LinkBoxComponent', () => { + let component: LinkBoxComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [provideNoopAnimations(), ], + imports: [LinkBoxComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LinkBoxComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/client/src/components/link-box/link-box.component.ts b/client/src/components/link-box/link-box.component.ts new file mode 100644 index 000000000..cd22b01c7 --- /dev/null +++ b/client/src/components/link-box/link-box.component.ts @@ -0,0 +1,64 @@ +import { CommonModule } from '@angular/common'; +import { Component, input, signal, effect, DestroyRef, inject, ChangeDetectionStrategy } from '@angular/core'; +import { IconDirective } from '@coreui/icons-angular'; +import { ButtonModule } from '@coreui/angular'; +import { cilLink, cibInstagram, cibTwitch } from '@coreui/icons'; + +@Component({ + selector: 'app-link-box', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + IconDirective, + ButtonModule + ], + templateUrl: './link-box.component.html', + styleUrl: './link-box.component.scss' +}) + +export class LinkBoxComponent { + private destroyRef = inject(DestroyRef); + + title = input(''); + linkName = input(''); + linkUrl = input(''); + icon = input(cilLink); + color = input('#000000'); // Default color + + isLargeScreen = signal(false); + + constructor() { + this.checkScreenSize(); + + // Use effect for proper cleanup + effect(() => { + const resizeHandler = () => { + const screenWidth = window.innerWidth; + this.isLargeScreen.set(screenWidth > 1000); + }; + + window.addEventListener('resize', resizeHandler); + + this.destroyRef.onDestroy(() => { + window.removeEventListener('resize', resizeHandler); + }); + }); + } + + private checkScreenSize() { + this.isLargeScreen.set(window.innerWidth > 1000); + } + + goTo() { + console.log('Link clicked:', this.linkUrl()); + if (this.linkUrl()) { + window.open(this.linkUrl(), '_blank'); + } +} +} + +export const icons = { + cilLink, + cibInstagram, + cibTwitch + }; \ No newline at end of file diff --git a/client/src/components/loading-spinner/loading-spinner.component.html b/client/src/components/loading-spinner/loading-spinner.component.html new file mode 100644 index 000000000..a302d5cf2 --- /dev/null +++ b/client/src/components/loading-spinner/loading-spinner.component.html @@ -0,0 +1,5 @@ +@if (loading()) { +
+ +
+} \ No newline at end of file diff --git a/client/src/components/loading-spinner/loading-spinner.component.scss b/client/src/components/loading-spinner/loading-spinner.component.scss new file mode 100644 index 000000000..48e12f6f7 --- /dev/null +++ b/client/src/components/loading-spinner/loading-spinner.component.scss @@ -0,0 +1,36 @@ +@use "@coreui/coreui/scss/mixins/color-scheme" as *; +@use "@coreui/coreui/scss/mixins/breakpoints"; + + +.loading-overlay { + position: fixed !important; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(100, 100, 100, 0.808); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + + } + + // .spinner { + // width: 50px; + // height: 50px; + // border: 6px solid #ccc; + // border-top: 6px solid #3f51b5; + // border-radius: 50%; + // animation: spin 1s linear infinite; + // } + + // @keyframes spin { + // to { transform: rotate(360deg); } + // } + + // @media (min-width: 576px) { + // .loading-overlay { + // margin-left: var(--sidebar-width); + // } + // } \ No newline at end of file diff --git a/client/src/components/loading-spinner/loading-spinner.component.ts b/client/src/components/loading-spinner/loading-spinner.component.ts new file mode 100644 index 000000000..dbd729a1d --- /dev/null +++ b/client/src/components/loading-spinner/loading-spinner.component.ts @@ -0,0 +1,17 @@ +import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { LoadingService } from '../../app/service/loading.service'; +import { SpinnerComponent } from '@coreui/angular'; + +@Component({ + selector: 'app-loading-spinner', + templateUrl: './loading-spinner.component.html', + styleUrls: ['./loading-spinner.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, SpinnerComponent] +}) +export class LoadingSpinnerComponent { + private loadingService = inject(LoadingService); + + loading = this.loadingService.loading; +} \ No newline at end of file diff --git a/src/components/public-api.ts b/client/src/components/public-api.ts similarity index 100% rename from src/components/public-api.ts rename to client/src/components/public-api.ts diff --git a/src/declarations.d.ts b/client/src/declarations.d.ts similarity index 100% rename from src/declarations.d.ts rename to client/src/declarations.d.ts diff --git a/client/src/environments/environment.prod.ts b/client/src/environments/environment.prod.ts new file mode 100644 index 000000000..1fd36bebc --- /dev/null +++ b/client/src/environments/environment.prod.ts @@ -0,0 +1,4 @@ +export const environment = { + production: true, + apiBaseUrl: '/api' +}; diff --git a/client/src/environments/environment.ts b/client/src/environments/environment.ts new file mode 100644 index 000000000..c0a903ce8 --- /dev/null +++ b/client/src/environments/environment.ts @@ -0,0 +1,4 @@ +export const environment = { + production: false, + apiBaseUrl: '/api' +}; diff --git a/client/src/index.html b/client/src/index.html new file mode 100644 index 000000000..f0ff1ccb3 --- /dev/null +++ b/client/src/index.html @@ -0,0 +1,154 @@ + + + + + + + + + + + + + F1 RaceForFederica + + + + + +
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+

+                        __                                 
+                  _.---.  |                                
+   .----.     _.-'   |/\| |.--.                            
+   |f123|__.-'  _Marlboro_|  |_)  _______________          
+   |  .-""-.""""  ___    `----'"))    __   .-""-.""""--._  
+   '-' ,--. `    |Lec|   .---.       |:.| ' ,--. `      _`.
+    ( (    ) ) __|#16|__ \\|// _..--  \/ ( (    ) )--._".-.
+     . `--' ;\__________________..--------. `--' ;--------'
+      `-..-'                               `-..-'          
+      
+
+
+
+ + + + + \ No newline at end of file diff --git a/src/main.ts b/client/src/main.ts similarity index 87% rename from src/main.ts rename to client/src/main.ts index 59c8f3775..2b85bde73 100644 --- a/src/main.ts +++ b/client/src/main.ts @@ -6,5 +6,4 @@ import { AppComponent } from './app/app.component'; import { appConfig } from './app/app.config'; bootstrapApplication(AppComponent, appConfig) - .catch(err => console.error(err)); - + .catch(err => console.error(err)); \ No newline at end of file diff --git a/src/scss/_charts.scss b/client/src/scss/_charts.scss similarity index 100% rename from src/scss/_charts.scss rename to client/src/scss/_charts.scss diff --git a/client/src/scss/_custom-datetime.scss b/client/src/scss/_custom-datetime.scss new file mode 100644 index 000000000..299406e13 --- /dev/null +++ b/client/src/scss/_custom-datetime.scss @@ -0,0 +1,45 @@ +// Enhanced Input Styling for Datetime Pickers +.global-datetime { + width: 100%; + padding: 0.5rem 1rem; + border: 2px solid var(--cui-border-color); + border-radius: 8px; + background: var(--cui-body-bg); + color: var(--cui-body-color); + font-size: 0.95rem; + transition: all 0.3s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + min-height: 48px; + + &:focus { + border-color: var(--cui-primary); + box-shadow: 0 0 0 0.2rem rgba(var(--cui-primary-rgb), 0.25); + outline: none; + } + + &:hover:not(:focus) { + border-color: rgba(var(--cui-primary-rgb), 0.5); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + } + + &:disabled { + background-color: var(--cui-gray-100); + color: var(--cui-gray-600); + cursor: not-allowed; + opacity: 0.7; + } + + // Style the calendar icon + &::-webkit-calendar-picker-indicator { + cursor: pointer; + opacity: 0.6; + transition: 0.2s; + padding: 5px; + + &:hover { + opacity: 1; + background-color: rgba(var(--cui-primary-rgb), 0.1); + border-radius: 4px; + } + } +} \ No newline at end of file diff --git a/client/src/scss/_custom-select.scss b/client/src/scss/_custom-select.scss new file mode 100644 index 000000000..b78f59f1e --- /dev/null +++ b/client/src/scss/_custom-select.scss @@ -0,0 +1,73 @@ +// Enhanced Select Styling +.global-select-group { + margin-bottom: 1.5rem; + position: relative; + + .global-select-label { + display: block; + margin-bottom: 0.5rem; + font-weight: 600; + font-size: 0.95rem; + color: var(--cui-body-color); + } + + .global-select { + width: 100%; + padding: 0.5rem 1rem; + border: 2px solid var(--cui-border-color); + border-radius: 8px; + background: var(--cui-body-bg); + color: var(--cui-body-color); + font-size: 0.95rem; + transition: all 0.3s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + min-height: 48px; + + &:focus { + border-color: var(--cui-primary); + box-shadow: 0 0 0 0.2rem rgba(var(--cui-primary-rgb), 0.25); + outline: none; + } + + &:hover:not(:focus) { + border-color: rgba(var(--cui-primary-rgb), 0.5); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + } + + &:disabled { + background-color: var(--cui-gray-100); + color: var(--cui-gray-600); + cursor: not-allowed; + opacity: 0.7; + } + + option { + padding: 0.5rem; + background: var(--cui-body-bg); + color: var(--cui-body-color); + } + } + + // Loading overlay for selector + .loading-overlay { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: rgba(var(--cui-body-bg-rgb), 0.95); + padding: 0.5rem; + border-radius: 6px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + display: flex; + align-items: center; + gap: 0.5rem; + margin-top: 0.5rem; + z-index: 10; + + .loading-text { + font-size: 0.85rem; + color: var(--cui-text-muted); + margin: 0; + } + } + } \ No newline at end of file diff --git a/client/src/scss/_custom.scss b/client/src/scss/_custom.scss new file mode 100644 index 000000000..8eeb95f9c --- /dev/null +++ b/client/src/scss/_custom.scss @@ -0,0 +1,34 @@ +// Here you can add other styles + +// custom .chartjs-tooltip-body-item padding +@use "charts"; + +// custom tweaks for scrollbar styling (wip) +@use "scrollbar"; + +// custom tweaks for select styling +@use "custom-select"; +// custom tweaks for datetime input styling +@use "custom-datetime"; + + // custom calendar today cell color +.calendar-cell.today { + --cui-calendar-cell-today-color: var(--cui-info) !important; +} + +// custom select week cursor pointer +.select-week .calendar-row.current { + cursor: pointer; +} +.sidebar-nav { --cui-sidebar-nav-link-icon-margin: 1rem; } + +.punti { + font-size: 1.2rem; + font-weight: bold; + color: #ffffff; + margin-top: 5px; +} + +.punti-table { + font-weight: bold; +} diff --git a/src/scss/_examples.scss b/client/src/scss/_examples.scss similarity index 84% rename from src/scss/_examples.scss rename to client/src/scss/_examples.scss index c0ee455d5..869892bdd 100644 --- a/src/scss/_examples.scss +++ b/client/src/scss/_examples.scss @@ -1,5 +1,6 @@ -/* stylelint-disable declaration-no-important, scss/selector-no-redundant-nesting-selector */ -$enable-deprecation-messages: false; /* stylelint-disable-line scss/dollar-variable-default */ +@use "@coreui/coreui/scss/variables" as *; +@use "@coreui/coreui/scss/mixins/breakpoints" as *; +@use "@coreui/coreui/scss/mixins/color-mode" as *; .example { &:not(:first-child) { @@ -8,7 +9,6 @@ $enable-deprecation-messages: false; /* stylelint-disable-line scss/dollar-varia .tab-content { background-color: var(--#{$prefix}tertiary-bg); - //background-color: rgba(var(--#{$prefix}tertiary-bg-rgb), var(--#{$prefix}bg-opacity, 1)) !important; } & + p { @@ -57,7 +57,6 @@ $enable-deprecation-messages: false; /* stylelint-disable-line scss/dollar-varia > .btn-group { margin: .25rem .125rem; } - > .btn-toolbar + .btn-toolbar { margin-top: .5rem; } @@ -107,3 +106,9 @@ $enable-deprecation-messages: false; /* stylelint-disable-line scss/dollar-varia } } } + +@include color-mode(dark) { + .example .tab-content { + background-color: var(--#{$prefix}secondary-bg); + } +} \ No newline at end of file diff --git a/src/scss/_fixes.scss b/client/src/scss/_fixes.scss similarity index 100% rename from src/scss/_fixes.scss rename to client/src/scss/_fixes.scss diff --git a/client/src/scss/_floating-forms.scss b/client/src/scss/_floating-forms.scss new file mode 100644 index 000000000..86a36da00 --- /dev/null +++ b/client/src/scss/_floating-forms.scss @@ -0,0 +1,422 @@ +// Common Floating Form Styles +// Shared styles for login dropdown and registration modal + +// Base input styling for floating forms +.floating-input-base { + border-radius: 8px; + border: 2px solid var(--cui-border-color); + padding: 1rem 0.75rem 0.25rem 0.75rem; + font-size: 0.95rem; + transition: all 0.3s ease; + background: var(--cui-body-bg); + height: calc(3.5rem + 2px); + line-height: 1.25; + + &:focus { + border-color: var(--cui-primary); + box-shadow: 0 0 0 0.2rem rgba(var(--cui-primary-rgb), 0.25); + background: var(--cui-body-bg); + outline: none; + } + + &:invalid { + border-color: var(--cui-danger); + } + + &:hover:not(:focus) { + border-color: rgba(var(--cui-primary-rgb), 0.5); + } +} + +// Base label styling for floating forms +.floating-label-base { + color: var(--cui-primary); + font-weight: 500; + font-size: 0.85rem; +} + +// Input group container for floating forms +// Unified class that supports both CoreUI and custom floating labels +.floating-input-group { + margin-bottom: 1.25rem; + position: relative; + + // Floating label container + &.form-floating { + position: relative; + } + + // Apply base styles to CoreUI inputs + input[cFormControl] { + @extend .floating-input-base; + } + + // Apply base styles to CoreUI labels + label { + @extend .floating-label-base; + } + + // Support for custom floating inputs (legacy) + .registration-input, + .login-input { + border-radius: 8px; + border: 2px solid var(--cui-border-color); + padding: 1rem 0.75rem 0.25rem 0.75rem; + font-size: 0.95rem; + transition: all 0.3s ease; + background: var(--cui-body-bg); + height: auto; + min-height: 58px; + + &:focus { + border-color: var(--cui-primary); + box-shadow: 0 0 0 0.2rem rgba(var(--cui-primary-rgb), 0.25); + background: var(--cui-body-bg); + outline: none; + } + + &::placeholder { + color: transparent; + opacity: 0; + } + + &:focus::placeholder { + color: var(--cui-text-muted); + opacity: 0.7; + } + + &:invalid { + border-color: var(--cui-danger); + } + + &:hover:not(:focus) { + border-color: rgba(var(--cui-primary-rgb), 0.5); + } + + // When input has value or is focused + &:focus, + &:not(:placeholder-shown) { + padding-top: 1.25rem; + padding-bottom: 0.25rem; + } + } + + // Custom floating label styling + .input-label { + position: absolute; + top: 0; + left: 0; + padding: 1rem 0.75rem; + pointer-events: none; + color: var(--cui-text-muted); + font-weight: 400; + font-size: 0.95rem; + margin-bottom: 0; + transform-origin: 0 0; + transition: all 0.3s ease; + z-index: 2; + + // When input is focused or has value + .registration-input:focus ~ &, + .registration-input:not(:placeholder-shown) ~ &, + .login-input:focus ~ &, + .login-input:not(:placeholder-shown) ~ & { + color: var(--cui-primary); + font-weight: 500; + font-size: 0.75rem; + transform: translateY(-0.5rem) scale(0.85); + padding: 0.25rem 0.75rem 0; + } + + // Alternative approach for floating labels + .form-floating & { + position: absolute; + top: 0; + left: 0; + padding: 1rem 0.75rem; + color: var(--cui-text-muted); + font-size: 0.95rem; + font-weight: 400; + pointer-events: none; + transform-origin: 0 0; + transition: all 0.3s ease; + } + } +} + +// CoreUI floating form overrides - common to all components +[cFormFloating] { + .form-control, + input[cFormControl] { + border-radius: 8px !important; + border: 2px solid var(--cui-border-color) !important; + transition: all 0.3s ease !important; + + &:focus { + border-color: var(--cui-primary) !important; + box-shadow: 0 0 0 0.2rem rgba(var(--cui-primary-rgb), 0.25) !important; + } + + &:hover:not(:focus) { + border-color: rgba(var(--cui-primary-rgb), 0.5) !important; + } + } + + label { + color: var(--cui-primary) !important; + font-weight: 500 !important; + font-size: 0.85rem !important; + } +} + +// Error message styling for forms +.form-error-message { + color: var(--cui-danger); + font-size: 0.85rem; + font-weight: 500; + margin-top: 0.5rem; + padding: 0.5rem; + background: rgba(var(--cui-danger-rgb), 0.1); + border-radius: 6px; + border-left: 3px solid var(--cui-danger); + display: flex; + align-items: center; + gap: 0.5rem; + + .error-icon { + width: 1rem; + height: 1rem; + flex-shrink: 0; + } +} + +// Success message styling for forms +.form-success-message { + color: var(--cui-success); + font-size: 0.85rem; + font-weight: 500; + margin-top: 0.5rem; + padding: 0.5rem; + background: rgba(var(--cui-success-rgb), 0.1); + border-radius: 6px; + border-left: 3px solid var(--cui-success); + display: flex; + align-items: center; + gap: 0.5rem; + + .success-icon { + width: 1rem; + height: 1rem; + flex-shrink: 0; + } +} + +// Common UI Component Patterns +// These patterns are reused across multiple components + +// Gradient header pattern (used in login dropdown and registration modal) +.gradient-header { + padding: 1.25rem 1.5rem 0.75rem; + background: linear-gradient(135deg, var(--cui-primary), var(--cui-info)); + color: white; + border-bottom: none; + + .title { + margin: 0; + font-size: 1.1rem; + font-weight: 600; + text-align: center; + color: white; + } + + .btn-close { + filter: brightness(0) invert(1); + opacity: 0.8; + + &:hover { + opacity: 1; + } + } +} + +// Enhanced button with hover effects +.enhanced-button { + padding: 0.75rem 1.5rem; + border-radius: 8px; + font-weight: 600; + transition: all 0.3s ease; + + &:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(var(--cui-primary-rgb), 0.3); + } + + &:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; + } + + // Variant for larger buttons + &.large { + padding: 0.875rem 2rem; + border-radius: 10px; + font-size: 1rem; + + &:hover:not(:disabled) { + box-shadow: 0 6px 20px rgba(var(--cui-primary-rgb), 0.3); + } + } + + // Variant for smaller buttons + &.small { + padding: 0.5rem 1rem; + border-radius: 6px; + font-size: 0.9rem; + } +} + +// Modal/Dropdown container pattern +.modal-dropdown-container { + border: none; + border-radius: 12px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15); + overflow: hidden; + background: var(--cui-body-bg); + + // Enhanced variant for larger modals + &.enhanced { + border-radius: 16px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2); + } +} + +// File input styling pattern +.file-input-group { + margin-bottom: 1.25rem; + + .file-label { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + border: 2px dashed var(--cui-border-color); + border-radius: 8px; + background: var(--cui-tertiary-bg); + color: var(--cui-text-muted); + font-weight: 500; + font-size: 0.95rem; + cursor: pointer; + transition: all 0.3s ease; + text-align: center; + justify-content: center; + + &:hover { + border-color: var(--cui-primary); + background: rgba(var(--cui-primary-rgb), 0.05); + color: var(--cui-primary); + } + + .file-icon { + flex-shrink: 0; + transition: transform 0.3s ease; + } + + &:hover .file-icon { + transform: scale(1.1); + } + } + + .file-input { + display: none; + } + + .file-selected { + margin-top: 0.75rem; + padding: 0.5rem 0.75rem; + background: rgba(var(--cui-success-rgb), 0.1); + border: 1px solid rgba(var(--cui-success-rgb), 0.3); + border-radius: 6px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + + .file-name { + color: var(--cui-success); + font-size: 0.9rem; + font-weight: 500; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .remove-file { + background: none; + border: none; + color: var(--cui-danger); + font-size: 1.25rem; + font-weight: bold; + cursor: pointer; + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.3s ease; + + &:hover { + background: rgba(var(--cui-danger-rgb), 0.1); + transform: scale(1.2); + } + } + } +} + +// Card container pattern (used in podium cards, albo containers, etc.) +.card-container { + background: var(--cui-body-bg); + border-radius: 16px; + border: 1px solid var(--cui-border-color); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + + // Variants + &.padded { + padding: 2rem; + } + + &.small { + border-radius: 12px; + padding: 1rem; + } + + &.large { + border-radius: 20px; + padding: 2.5rem; + } +} + +// Toggle button pattern (login toggle, etc.) +.toggle-button { + padding: 0.5rem 0.75rem; + font-size: 0.875rem; + border-radius: 50px; + transition: all 0.3s ease; + background: transparent; + border: 1px solid var(--cui-border-color); + + &:hover { + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + } + + &:focus { + box-shadow: 0 0 0 2px rgba(var(--cui-primary-rgb), 0.25); + outline: none; + } +} + + diff --git a/src/scss/_scrollbar.scss b/client/src/scss/_scrollbar.scss similarity index 56% rename from src/scss/_scrollbar.scss rename to client/src/scss/_scrollbar.scss index b560f583e..3333bf56b 100644 --- a/src/scss/_scrollbar.scss +++ b/client/src/scss/_scrollbar.scss @@ -26,23 +26,4 @@ ::-webkit-scrollbar-thumb:hover { background: #444; -} - -.dark-theme::-webkit-scrollbar-thumb { - background: var(--cui-gray-600, #444); -} - -.dark-theme::-webkit-scrollbar-thumb:hover { - background: var(--cui-gray-400, #999); -} - -.ng-scroll-content { - display: flex !important; -} - -.ng-scrollbar:not(.overflow) .ng-scrollbar-wrapper[verticalused="false"] { - //background-color: #e797a5; - .ng-scroll-viewport { - display: flex; - } -} +} \ No newline at end of file diff --git a/src/scss/_theme.scss b/client/src/scss/_theme.scss similarity index 68% rename from src/scss/_theme.scss rename to client/src/scss/_theme.scss index 49e1c79e6..1fef2a5cf 100644 --- a/src/scss/_theme.scss +++ b/client/src/scss/_theme.scss @@ -1,11 +1,13 @@ +@use "@coreui/coreui/scss/mixins/transition" as *; +@use "@coreui/coreui/scss/mixins/color-mode" as *; + body { background-color: var(--cui-tertiary-bg); } .wrapper { width: 100%; - @include ltr-rtl("padding-left", var(--cui-sidebar-occupy-start, 0)); - @include ltr-rtl("padding-right", var(--cui-sidebar-occupy-end, 0)); + padding-inline: var(--cui-sidebar-occupy-start, 0) var(--cui-sidebar-occupy-end, 0); will-change: auto; @include transition(padding .15s); } @@ -33,13 +35,13 @@ body { } .sidebar-toggler { - @include ltr-rtl("margin-left", auto); + margin-inline-start: auto; } .sidebar-narrow, .sidebar-narrow-unfoldable:not(:hover) { .sidebar-toggler { - @include ltr-rtl("margin-right", auto); + margin-inline-end: auto; } } @@ -51,14 +53,12 @@ body { min-height: calc(3rem + 1px); // stylelint-disable-line function-disallowed-list } -@if $enable-dark-mode { - @include color-mode(dark) { - body { - background-color: var(--cui-dark-bg-subtle); - } +@include color-mode(dark) { + body { + background-color: var(--cui-dark-bg-subtle); + } - .footer { - --cui-footer-bg: var(--cui-body-bg); - } + .footer { + --cui-footer-bg: var(--cui-body-bg); } -} +} \ No newline at end of file diff --git a/src/scss/_variables.scss b/client/src/scss/_variables.scss similarity index 69% rename from src/scss/_variables.scss rename to client/src/scss/_variables.scss index b0f8a52a3..5db59014b 100644 --- a/src/scss/_variables.scss +++ b/client/src/scss/_variables.scss @@ -3,3 +3,6 @@ // If you want to customize your project please add your variables below. $enable-deprecation-messages: false !default; + +// Imposta la dimensione base del font +$font-size-base: 1.1rem; \ No newline at end of file diff --git a/src/app/views/base/breadcrumbs/breadcrumbs.component.scss b/client/src/scss/_vote-history-table.scss similarity index 100% rename from src/app/views/base/breadcrumbs/breadcrumbs.component.scss rename to client/src/scss/_vote-history-table.scss diff --git a/client/src/scss/styles.scss b/client/src/scss/styles.scss new file mode 100644 index 000000000..8f09bc0f3 --- /dev/null +++ b/client/src/scss/styles.scss @@ -0,0 +1,35 @@ +/* You can add global styles to this file, and also import other style files */ + +// Import styles with default layout. +@use "@coreui/coreui/scss/coreui" as * with ( + $enable-deprecation-messages: false +); + +// Import Chart.js custom tooltips styles +@use "@coreui/chartjs/scss/coreui-chartjs"; + +// Custom styles for this theme +@use "theme"; + +// Some temp fixes +//@use "fixes"; + +// If you want to add custom CSS you can put it here. +@use "custom"; + +// Examples +// We use those styles to show code examples, you should remove them in your application. +@use "examples"; + +body.modal-open { + /* Disable pointer events for everything by default */ + pointer-events: none; +} + +body.modal-open .modal { /* Target the CoreUI modal */ + pointer-events: auto; /* Re-enable pointer events for the modal */ +} + +body.modal-open .modal * { /* Re-enable pointer events for modal content */ + pointer-events: auto; +} \ No newline at end of file diff --git a/src/test.ts b/client/src/test.ts similarity index 63% rename from src/test.ts rename to client/src/test.ts index 0c8df5a86..c73b42631 100644 --- a/src/test.ts +++ b/client/src/test.ts @@ -3,13 +3,13 @@ import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { - BrowserDynamicTestingModule, - platformBrowserDynamicTesting -} from '@angular/platform-browser-dynamic/testing'; + BrowserTestingModule, + platformBrowserTesting +} from '@angular/platform-browser/testing'; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( - BrowserDynamicTestingModule, - platformBrowserDynamicTesting(), + BrowserTestingModule, + platformBrowserTesting(), { teardown: { destroyAfterEach: true }}, ); diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json new file mode 100644 index 000000000..d7729539a --- /dev/null +++ b/client/tsconfig.app.json @@ -0,0 +1,32 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [ + "@angular/localize" + ], + "paths": { + "@docs-components/*": [ + "./src/components/*" + ], + "@f123dashboard/shared": [ + "../shared/src/index.ts" + ], + "jszip": [ + "./node_modules/jszip/dist/jszip.min.js" + ], + "node_modules/chart.js/dist/types/basic": [ + "../node_modules/chart.js/dist/types/basic.d.ts" + ], + "node_modules/chart.js/dist/types/utils": [ + "../node_modules/chart.js/dist/types/utils.d.ts" + ] + } + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/tsconfig.json b/client/tsconfig.json similarity index 94% rename from tsconfig.json rename to client/tsconfig.json index d1a8504c2..918d15de3 100644 --- a/tsconfig.json +++ b/client/tsconfig.json @@ -11,10 +11,11 @@ "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "importHelpers": true, "target": "ES2022", "module": "ES2022", diff --git a/tsconfig.spec.json b/client/tsconfig.spec.json similarity index 100% rename from tsconfig.spec.json rename to client/tsconfig.spec.json diff --git a/docs/api-configuration.md b/docs/api-configuration.md new file mode 100644 index 000000000..13229703c --- /dev/null +++ b/docs/api-configuration.md @@ -0,0 +1,142 @@ +# API and Configuration Services + +This document describes the improved API and configuration management system in the F1 Dashboard application. + +## Overview + +The application now uses a centralized approach for handling: + +- API base URLs and endpoints +- Environment-specific configurations +- HTTP requests with consistent headers + +## Services + +### ConfigService + +Manages application-wide configuration settings based on the environment. + +**Key Features:** + +- Environment-based configuration loading +- Production/development mode detection +- Token refresh timing configuration + +**Usage:** + +```typescript +import { ConfigService } from './config.service'; + +constructor(private configService: ConfigService) {} + +// Get API base URL +const baseUrl = this.configService.apiBaseUrl; + +// Check if production +const isProd = this.configService.isProduction; +``` + +### ApiService + +Provides a centralized HTTP client for making API requests. + +**Key Features:** + +- Automatic base URL management +- Consistent header creation +- Type-safe HTTP methods +- Authentication header helpers + +**Usage:** + +```typescript +import { ApiService } from './api.service'; + +constructor(private apiService: ApiService) {} + +// Make authenticated POST request +const response = await firstValueFrom( + this.apiService.post('AuthService/login', requestBody) +); + +// Get endpoint URL +const url = this.apiService.getEndpointUrl('AuthService/register'); +``` + +## Environment Configuration + +### Development (environment.ts) + +```typescript +export const environment = { + production: false, + apiBaseUrl: 'http://localhost:8083' +}; +``` + +### Production (environment.prod.ts) + +```typescript +export const environment = { + production: true, + apiBaseUrl: 'https://f123dashboard.app.genez.io' +}; +``` + +## Benefits + +### Before (Problems) + +- Hardcoded URL logic scattered across services +- Duplicate environment detection code +- Inconsistent header creation + +### After (Solutions) + +- ✅ Centralized configuration management +- ✅ Single source of truth for API URLs +- ✅ Consistent HTTP request handling +- ✅ Type-safe API calls +- ✅ Easy to test and mock +- ✅ Configurable token refresh timing + +## Migration Guide + +### For New Services + +1. Inject `ApiService` instead of `HttpClient` +2. Use `apiService.post()`, `apiService.get()`, etc. +3. Use `ConfigService` for configuration values + +### For Existing Services + +1. Replace hardcoded base URL logic with `ApiService` +2. Use `apiService.createHeaders()` for consistent headers + +## Testing + +Both services include comprehensive unit tests: + +- `api.service.spec.ts` +- `config.service.spec.ts` + +Run tests with: + +```bash +ng test +``` + +## Angular Configuration + +The `angular.json` file is configured to automatically replace environment files during production builds: + +```json +"fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } +] +``` + +This ensures the correct configuration is used for each environment without code changes. diff --git a/docs/authentication-improvements.md b/docs/authentication-improvements.md new file mode 100644 index 000000000..6a32a4f61 --- /dev/null +++ b/docs/authentication-improvements.md @@ -0,0 +1,308 @@ +# Authentication System Improvements + +## Executive Summary + +The current authentication system has been significantly enhanced from a basic client-side implementation to a secure, token-based system with proper backend authentication. This document outlines all the improvements made and provides implementation guidance. + +## Current Issues (Before Improvements) + +### 1. **Critical Security Vulnerabilities** +- **Plain text passwords**: All passwords stored and transmitted without encryption +- **Client-side authentication**: User credentials stored in browser sessionStorage +- **No password hashing**: Passwords completely exposed in database +- **Hard-coded users**: User data defined in static arrays +- **No session management**: No token expiration or refresh mechanisms +- **No input validation**: Weak password policies and no sanitization + +### 2. **Architecture Problems** +- **Mixed authentication logic**: Auth guard and service checking different state +- **No proper error handling**: Generic error messages and no logging +- **No user management**: No registration, password reset, or user profile features +- **Inconsistent state**: Multiple sources of truth for authentication state + +### 3. **User Experience Issues** +- **No feedback**: Poor error messages and no loading states +- **No registration flow**: Users cannot create accounts +- **No password management**: Cannot change or reset passwords +- **No remember me**: Session lost on browser close + +## Implemented Improvements + +### 1. **Backend Authentication Service** (`server/src/auth_interface.ts`) + +**Features:** +- JWT token-based authentication +- Password hashing with crypto +- User registration and login +- Token validation and refresh +- Password change functionality +- Input validation and sanitization +- Proper error handling and logging + +**Security Features:** +- Password strength validation (uppercase, lowercase, numbers, special characters) +- Username format validation (alphanumeric and underscore only) +- Token expiration (7 days) +- Secure password hashing +- SQL injection prevention + +**API Methods:** +```typescript +// Login user +async login(username: string, password: string): Promise + +// Register new user +async register(username: string, name: string, surname: string, password: string): Promise + +// Validate JWT token +async validateToken(token: string): Promise + +// Change user password +async changePassword(userId: number, currentPassword: string, newPassword: string): Promise + +// Refresh JWT token +async refreshToken(oldToken: string): Promise +``` + +### 2. **Frontend Authentication Service** (`client/src/app/service/auth.service.ts`) + +**Features:** +- Observable-based state management +- Automatic token refresh +- Secure token storage +- Session persistence +- Reactive authentication state +- Proper error handling + +**Key Improvements:** +- Uses RxJS BehaviorSubject for reactive state +- Automatic token validation on app start +- Scheduled token refresh before expiration +- Proper logout and cleanup +- Return URL handling for protected routes + +**Usage Example:** +```typescript +// Check authentication status +this.authService.isAuthenticated$.subscribe(isAuth => { + console.log('User authenticated:', isAuth); +}); + +// Get current user +this.authService.currentUser$.subscribe(user => { + console.log('Current user:', user); +}); + +// Login +const response = await this.authService.login({ username, password }); +``` + +### 3. **Improved Authentication Guard** (`client/src/app/guard/auth.guard.ts`) + +**Features:** +- Observable-based guard using RxJS +- Automatic redirect to login page +- Return URL preservation +- Proper route protection + +**How it works:** +```typescript +// Uses the auth service's reactive state +return authService.isAuthenticated$.pipe( + take(1), + map(isAuthenticated => { + if (!isAuthenticated) { + sessionStorage.setItem('returnUrl', state.url); + router.navigate(['/login']); + return false; + } + return true; + }) +); +``` + +### 4. **Enhanced Login Component** (`client/src/app/views/login/login.component.ts`) + +**Features:** +- Separate login and registration forms +- Comprehensive form validation +- Loading states and error handling +- Password strength validation +- User-friendly error messages + +**Form Validation:** +- Required field validation +- Username format validation +- Password strength requirements +- Confirm password matching +- Real-time validation feedback + +### 5. **Database Schema Improvements** (`server/scripts/auth_migration.sql`) + +**New Columns:** +- `created_at`: User creation timestamp +- `last_login`: Last login tracking +- `password_updated_at`: Password change tracking +- `is_active`: Account status + +**Additional Tables:** +- `user_sessions`: Session management + +## Implementation Steps + +### 1. **Backend Setup** +```bash +# Install required dependencies +cd server +npm install jsonwebtoken @types/jsonwebtoken + +# Run database migration +psql -d your_database -f scripts/auth_migration.sql + +# Deploy backend +genezio deploy --backend +``` + +### 2. **Frontend Setup** +```bash +# Install updated SDK +cd client +npm add @genezio-sdk/f123dashboard@latest + +# The authentication service and guard are already updated +``` + +### 3. **Environment Variables** +Add to your backend environment: +```env +JWT_SECRET=your-very-secure-secret-key-here +``` + +### 4. **Update Existing Components** +Replace any direct sessionStorage checks with the new auth service: + +**Before:** +```typescript +const isLoggedIn = sessionStorage.getItem('isLoggedIn') === 'true'; +``` + +**After:** +```typescript +this.authService.isAuthenticated$.subscribe(isAuth => { + // Handle authentication state +}); +``` + +## Security Best Practices Implemented + +### 1. **Password Security** +- Password hashing with SHA-256 +- Minimum 8 character length +- Complexity requirements (uppercase, lowercase, numbers, special chars) +- No password storage in plain text + +### 2. **Token Security** +- JWT tokens with expiration +- Automatic token refresh +- Secure token storage +- Token validation on each request + +### 3. **Session Management** +- Automatic session cleanup +- Token expiration handling +- Secure logout process +- Session persistence across browser sessions + +### 4. **Input Validation** +- Server-side validation +- SQL injection prevention +- XSS protection +- Input sanitization + +## Advanced Features for Future Implementation + +### 1. **Enhanced Security** +- **Rate limiting**: Prevent brute force attacks +- **2FA**: Two-factor authentication +- **Password reset**: Password recovery mechanism +- **Session management**: Multiple device sessions + +### 2. **User Management** +- **User profiles**: Editable user information +- **Role-based access**: Different permission levels +- **User administration**: Admin panel for user management +- **Activity logging**: User action tracking + +### 3. **Performance Optimizations** +- **Token caching**: Reduce validation requests +- **Background refresh**: Seamless token updates +- **Offline support**: Handle network disconnections +- **Progressive loading**: Faster initial load times + +## Migration Guide + +### 1. **Database Migration** +Run the provided SQL script to update your database schema: +```sql +-- Add new columns for proper authentication +ALTER TABLE fanta_player ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT NOW(); +-- ... (run the complete migration script) +``` + +### 2. **Existing User Data** +Hash existing passwords (if any): +```sql +-- Update existing users with hashed passwords +UPDATE fanta_player SET password = encode(digest(password, 'sha256'), 'hex'); +``` + +### 3. **Component Updates** +Update any components using the old auth service: +```typescript +// Replace direct service calls with observable subscriptions +this.authService.currentUser$.subscribe(user => { + this.currentUser = user; +}); +``` + +## Testing Strategy + +### 1. **Unit Tests** +- Test authentication service methods +- Test form validation logic +- Test guard functionality +- Test error handling + +### 2. **Integration Tests** +- Test login/logout flow +- Test token refresh process +- Test route protection +- Test session persistence + +### 3. **Security Tests** +- Test password strength validation +- Test token expiration handling +- Test unauthorized access attempts +- Test session security + +## Monitoring and Logging + +### 1. **Authentication Events** +- Login attempts (success/failure) +- Token validation failures +- Password changes + +### 2. **Security Metrics** +- Token refresh frequency +- Session duration analysis +- Password strength distribution + +## Conclusion + +The authentication system has been completely overhauled to provide: +- **Security**: Proper password hashing, token-based auth, input validation +- **User Experience**: Better error messages, loading states, form validation +- **Maintainability**: Clean architecture, reactive state management +- **Scalability**: Database-backed users, session management, proper APIs + +This implementation provides a solid foundation for a secure, user-friendly authentication system that can be extended with additional features as needed. diff --git a/docs/database-maintenance.md b/docs/database-maintenance.md new file mode 100644 index 000000000..57c59b9a2 --- /dev/null +++ b/docs/database-maintenance.md @@ -0,0 +1,112 @@ +# Simplified Database Maintenance Functions + +## Overview + +The authentication system includes a simple database maintenance function to keep the system performant by removing expired user sessions. + +## Available Function + +### `clean_expired_sessions()` +- **Purpose**: Removes expired user sessions from the `user_sessions` table +- **Logic**: Deletes sessions where `expires_at < NOW()` +- **Returns**: Number of deleted records +- **Frequency**: Should run daily or multiple times per day + +## Usage Methods + +### Method 1: Manual Database Execution + +```sql +-- Run immediately to clean expired sessions +SELECT clean_expired_sessions(); + +-- Check the result +SELECT 'Sessions cleaned' as operation, clean_expired_sessions() as deleted_count; +``` + +### Method 2: System Cron Job + +Create a shell script for automated cleanup: + +```bash +#!/bin/bash +# File: /path/to/cleanup-sessions.sh + +# Database connection details +DB_HOST="your-db-host" +DB_NAME="your-db-name" +DB_USER="your-db-user" +DB_PASSWORD="your-db-password" + +# Run session cleanup +psql -h $DB_HOST -U $DB_USER -d $DB_NAME -c "SELECT clean_expired_sessions();" + +# Log the result +echo "$(date): Session cleanup completed" >> /var/log/session-cleanup.log +``` + +Then add to crontab: +```bash +# Run every 4 hours +0 */4 * * * /path/to/cleanup-sessions.sh +``` + +### Method 3: PostgreSQL Scheduled Jobs (if pg_cron is available) + +```sql +-- Schedule session cleanup every 4 hours +SELECT cron.schedule('session-cleanup', '0 */4 * * *', 'SELECT clean_expired_sessions();'); + +-- View scheduled jobs +SELECT * FROM cron.job; + +-- Remove a scheduled job +SELECT cron.unschedule('session-cleanup'); +``` + +## Recommended Implementation + +### Quick Start + +1. **Test the function manually**: + ```sql + SELECT clean_expired_sessions(); + ``` + +2. **Set up automated cleanup**: + ```bash + # Add to crontab (crontab -e) + 0 */4 * * * psql -h your-host -U your-user -d your-database -c "SELECT clean_expired_sessions();" + ``` + +### Monitoring + +Check how many sessions are being cleaned: +```sql +-- Check current expired sessions +SELECT COUNT(*) as expired_sessions +FROM user_sessions +WHERE expires_at < NOW(); + +-- Check total sessions +SELECT COUNT(*) as total_sessions +FROM user_sessions; +``` + +## Performance Tips + +1. **Run during low-usage hours** if you have high session volume +2. **Monitor execution times** - if they get too long, increase frequency +3. **Check table size** before and after cleanup + +## Emergency Cleanup + +If the sessions table gets too large: +```sql +-- Clean in batches to avoid blocking +DELETE FROM user_sessions +WHERE expires_at < NOW() - INTERVAL '1 day' +LIMIT 10000; +``` + +This simplified approach focuses on the essential session cleanup functionality while maintaining system performance. diff --git a/package-lock.json b/package-lock.json index 85f8aa02b..44e3a5d70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,1881 +1,1752 @@ { - "name": "coreui-free-angular-admin-template", - "version": "5.2.16", + "name": "f123dashboard", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "coreui-free-angular-admin-template", - "version": "5.2.16", - "license": "MIT", - "dependencies": { - "@angular/animations": "^18.2.1", - "@angular/cdk": "^18.2.1", - "@angular/common": "^18.2.1", - "@angular/compiler": "^18.2.1", - "@angular/core": "^18.2.1", - "@angular/forms": "^18.2.1", - "@angular/language-service": "^18.2.1", - "@angular/platform-browser": "^18.2.1", - "@angular/platform-browser-dynamic": "^18.2.1", - "@angular/router": "^18.2.1", - "@coreui/angular": "~5.2.16", - "@coreui/angular-chartjs": "~5.2.16", - "@coreui/chartjs": "~4.0.0", - "@coreui/coreui": "~5.1.2", + "name": "f123dashboard", + "version": "2.0.0", + "workspaces": [ + "client", + "server", + "shared" + ], + "dependencies": { + "-": "^0.0.1", + "@coreui/coreui": "~5.5.0", + "g": "^2.0.1", + "nodemailer": "^8.0.0" + }, + "devDependencies": { + "@playwright/test": "^1.54.2", + "@types/node": "^25.2.1", + "concurrently": "^9.2.1", + "sass": "^1.97.0" + } + }, + "client": { + "name": "race-for-federica-dashboard", + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@angular/animations": "^21.0.6", + "@angular/cdk": "^21.0.5", + "@angular/common": "^21.0.6", + "@angular/compiler": "^21.0.6", + "@angular/core": "^21.0.6", + "@angular/forms": "^21.0.6", + "@angular/language-service": "^21.0.6", + "@angular/localize": "^21.0.6", + "@angular/material": "^21.0.0", + "@angular/platform-browser": "^21.0.6", + "@angular/platform-browser-dynamic": "^21.0.6", + "@angular/router": "^21.0.6", + "@coreui/angular": "~5.6.4", + "@coreui/angular-chartjs": "~5.6.4", + "@coreui/chartjs": "^4.2.0", + "@coreui/coreui": "~5.5.0", "@coreui/icons": "^3.0.1", - "@coreui/icons-angular": "~5.2.16", + "@coreui/icons-angular": "~5.6.4", "@coreui/utils": "^2.0.2", - "chart.js": "^4.4.4", - "lodash-es": "^4.17.21", - "ngx-scrollbar": "^13.0.3", - "rxjs": "~7.8.1", - "tslib": "^2.7.0", - "zone.js": "~0.14.10" + "@f123dashboard/shared": "file:../shared", + "@ng-bootstrap/ng-bootstrap": "^20.0.0", + "angularx-flatpickr": "^8.0.0", + "chart.js": "^4.5.1", + "flag-icons": "^7.2.3", + "flatpickr": "^4.6.13", + "lodash-es": "^4.17.22", + "ngx-scrollbar": "^19.1.4", + "rxjs": "~7.8.2", + "tslib": "^2.8.1", + "zone.js": "~0.16.0" }, "devDependencies": { - "@angular-devkit/build-angular": "^18.2.1", - "@angular/cli": "^18.2.1", - "@angular/compiler-cli": "^18.2.1", - "@angular/localize": "^18.2.1", - "@types/jasmine": "^5.1.4", + "@angular/build": "^21.0.4", + "@angular/cli": "^21.0.4", + "@angular/compiler-cli": "^21.0.6", + "@eslint/js": "^9.18.0", + "@types/jasmine": "^6.0.0", "@types/lodash-es": "^4.17.12", - "@types/node": "^20.16.1", - "jasmine-core": "^5.2.0", + "@types/node": "^25.2.1", + "angular-eslint": "^21.2.0", + "eslint": "^9.18.0", + "jasmine-core": "^6.0.1", "karma": "^6.4.4", "karma-chrome-launcher": "^3.2.0", "karma-coverage": "^2.2.1", "karma-jasmine": "^5.1.0", "karma-jasmine-html-reporter": "^2.1.0", - "typescript": "~5.5.4" + "typescript": "~5.9.0", + "typescript-eslint": "^8.19.1" }, "engines": { - "node": "^18.19.0 || ^20.9.0", - "npm": ">= 9" + "node": "^20.19.0 || ^22.12.0 || ^24.0.0", + "npm": ">=10" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "node_modules/-": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/-/-/--0.0.1.tgz", + "integrity": "sha512-3HfneK3DGAm05fpyj20sT3apkNcvPpCuccOThOPdzz8sY7GgQGe0l93XH9bt+YzibcTIgUAIMoyVJI740RtgyQ==", + "license": "UNLICENSED" + }, + "node_modules/@algolia/abtesting": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.12.2.tgz", + "integrity": "sha512-oWknd6wpfNrmRcH0vzed3UPX0i17o4kYLM5OMITyMVM2xLgaRbIafoxL0e8mcrNNb0iORCJA0evnNDKRYth5WQ==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" }, "engines": { - "node": ">=6.0.0" + "node": ">= 14.0.0" } }, - "node_modules/@angular-devkit/architect": { - "version": "0.1802.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.1.tgz", - "integrity": "sha512-XTnJfCBMDQl3xF4w/eNrq821gbj2Ig1cqbzpRflhz4pqrANTAfHfPoIC7piWEZ60FNlHapzb6fvh6tJUGXG9og==", + "node_modules/@algolia/client-abtesting": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.46.2.tgz", + "integrity": "sha512-oRSUHbylGIuxrlzdPA8FPJuwrLLRavOhAmFGgdAvMcX47XsyM+IOGa9tc7/K5SPvBqn4nhppOCEz7BrzOPWc4A==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.1", - "rxjs": "7.8.1" + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" + "node": ">= 14.0.0" } }, - "node_modules/@angular-devkit/build-angular": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.1.tgz", - "integrity": "sha512-ANsTWKjIlEvJ6s276TbwnDhkoHhQDfsNiRFUDRGBZu94UNR78ImQZSyKYGHJOeQQH6jpBtraA1rvW5WKozAtlw==", + "node_modules/@algolia/client-analytics": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.46.2.tgz", + "integrity": "sha512-EPBN2Oruw0maWOF4OgGPfioTvd+gmiNwx0HmD9IgmlS+l75DatcBkKOPNJN+0z3wBQWUO5oq602ATxIfmTQ8bA==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.1", - "@angular-devkit/build-webpack": "0.1802.1", - "@angular-devkit/core": "18.2.1", - "@angular/build": "18.2.1", - "@babel/core": "7.25.2", - "@babel/generator": "7.25.0", - "@babel/helper-annotate-as-pure": "7.24.7", - "@babel/helper-split-export-declaration": "7.24.7", - "@babel/plugin-transform-async-generator-functions": "7.25.0", - "@babel/plugin-transform-async-to-generator": "7.24.7", - "@babel/plugin-transform-runtime": "7.24.7", - "@babel/preset-env": "7.25.3", - "@babel/runtime": "7.25.0", - "@discoveryjs/json-ext": "0.6.1", - "@ngtools/webpack": "18.2.1", - "@vitejs/plugin-basic-ssl": "1.1.0", - "ansi-colors": "4.1.3", - "autoprefixer": "10.4.20", - "babel-loader": "9.1.3", - "browserslist": "^4.21.5", - "copy-webpack-plugin": "12.0.2", - "critters": "0.0.24", - "css-loader": "7.1.2", - "esbuild-wasm": "0.23.0", - "fast-glob": "3.3.2", - "http-proxy-middleware": "3.0.0", - "https-proxy-agent": "7.0.5", - "istanbul-lib-instrument": "6.0.3", - "jsonc-parser": "3.3.1", - "karma-source-map-support": "1.4.0", - "less": "4.2.0", - "less-loader": "12.2.0", - "license-webpack-plugin": "4.0.2", - "loader-utils": "3.3.1", - "magic-string": "0.30.11", - "mini-css-extract-plugin": "2.9.0", - "mrmime": "2.0.0", - "open": "10.1.0", - "ora": "5.4.1", - "parse5-html-rewriting-stream": "7.0.0", - "picomatch": "4.0.2", - "piscina": "4.6.1", - "postcss": "8.4.41", - "postcss-loader": "8.1.1", - "resolve-url-loader": "5.0.0", - "rxjs": "7.8.1", - "sass": "1.77.6", - "sass-loader": "16.0.0", - "semver": "7.6.3", - "source-map-loader": "5.0.0", - "source-map-support": "0.5.21", - "terser": "5.31.6", - "tree-kill": "1.2.2", - "tslib": "2.6.3", - "vite": "5.4.0", - "watchpack": "2.4.1", - "webpack": "5.93.0", - "webpack-dev-middleware": "7.3.0", - "webpack-dev-server": "5.0.4", - "webpack-merge": "6.0.1", - "webpack-subresource-integrity": "5.1.0" + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "optionalDependencies": { - "esbuild": "0.23.0" - }, - "peerDependencies": { - "@angular/compiler-cli": "^18.0.0", - "@angular/localize": "^18.0.0", - "@angular/platform-server": "^18.0.0", - "@angular/service-worker": "^18.0.0", - "@web/test-runner": "^0.18.0", - "browser-sync": "^3.0.2", - "jest": "^29.5.0", - "jest-environment-jsdom": "^29.5.0", - "karma": "^6.3.0", - "ng-packagr": "^18.0.0", - "protractor": "^7.0.0", - "tailwindcss": "^2.0.0 || ^3.0.0", - "typescript": ">=5.4 <5.6" - }, - "peerDependenciesMeta": { - "@angular/localize": { - "optional": true - }, - "@angular/platform-server": { - "optional": true - }, - "@angular/service-worker": { - "optional": true - }, - "@web/test-runner": { - "optional": true - }, - "browser-sync": { - "optional": true - }, - "jest": { - "optional": true - }, - "jest-environment-jsdom": { - "optional": true - }, - "karma": { - "optional": true - }, - "ng-packagr": { - "optional": true - }, - "protractor": { - "optional": true - }, - "tailwindcss": { - "optional": true - } + "node": ">= 14.0.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@angular-devkit/build-webpack": { - "version": "0.1802.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.1.tgz", - "integrity": "sha512-xOP9Hxkj/mWYdMTa/8uNxFTv7z+3UiGdt4VAO7vetV5qkU/S9rRq8FEKviCc2llXfwkhInSgeeHpWKdATa+YIQ==", + "node_modules/@algolia/client-common": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.46.2.tgz", + "integrity": "sha512-Hj8gswSJNKZ0oyd0wWissqyasm+wTz1oIsv5ZmLarzOZAp3vFEda8bpDQ8PUhO+DfkbiLyVnAxsPe4cGzWtqkg==", "dev": true, "license": "MIT", - "dependencies": { - "@angular-devkit/architect": "0.1802.1", - "rxjs": "7.8.1" - }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "webpack": "^5.30.0", - "webpack-dev-server": "^5.0.2" + "node": ">= 14.0.0" } }, - "node_modules/@angular-devkit/core": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.1.tgz", - "integrity": "sha512-fSuGj6CxiTFR+yjuVcaWqaVb5Wts39CSBYRO1BlsOlbuWFZ2NKC/BAb5bdxpB31heCBJi7e3XbPvcMMJIcnKlA==", + "node_modules/@algolia/client-insights": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.46.2.tgz", + "integrity": "sha512-6dBZko2jt8FmQcHCbmNLB0kCV079Mx/DJcySTL3wirgDBUH7xhY1pOuUTLMiGkqM5D8moVZTvTdRKZUJRkrwBA==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "8.17.1", - "ajv-formats": "3.0.1", - "jsonc-parser": "3.3.1", - "picomatch": "4.0.2", - "rxjs": "7.8.1", - "source-map": "0.7.4" + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } + "node": ">= 14.0.0" } }, - "node_modules/@angular-devkit/schematics": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.1.tgz", - "integrity": "sha512-2t/q0Jcv7yqhAzEdNgsxoGSCmPgD4qfnVOJ7EJw3LNIA+kX1CmtN4FESUS0i49kN4AyNJFAI5O2pV8iJiliKaw==", + "node_modules/@algolia/client-personalization": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.46.2.tgz", + "integrity": "sha512-1waE2Uqh/PHNeDXGn/PM/WrmYOBiUGSVxAWqiJIj73jqPqvfzZgzdakHscIVaDl6Cp+j5dwjsZ5LCgaUr6DtmA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.1", - "jsonc-parser": "3.3.1", - "magic-string": "0.30.11", - "ora": "5.4.1", - "rxjs": "7.8.1" + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" + "node": ">= 14.0.0" } }, - "node_modules/@angular/animations": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.1.tgz", - "integrity": "sha512-jit452yuE6DMVV09E6RAjgapgw64mMVH31ccpPvMDekzPsTuP3KNKtgRFU/k2DFhYJvyczM1AqqlgccE/JGaRw==", + "node_modules/@algolia/client-query-suggestions": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.46.2.tgz", + "integrity": "sha512-EgOzTZkyDcNL6DV0V/24+oBJ+hKo0wNgyrOX/mePBM9bc9huHxIY2352sXmoZ648JXXY2x//V1kropF/Spx83w==", + "dev": true, "license": "MIT", "dependencies": { - "tslib": "^2.3.0" + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/core": "18.2.1" + "node": ">= 14.0.0" } }, - "node_modules/@angular/build": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.1.tgz", - "integrity": "sha512-HwzjB+I31cAtjTTbbS2NbayzfcWthaKaofJlSmZIst3PN+GwLZ8DU0DRpd/xu5AXkk+DoAIWd+lzUIaqngz6ow==", + "node_modules/@algolia/client-search": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.46.2.tgz", + "integrity": "sha512-ZsOJqu4HOG5BlvIFnMU0YKjQ9ZI6r3C31dg2jk5kMWPSdhJpYL9xa5hEe7aieE+707dXeMI4ej3diy6mXdZpgA==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.1", - "@babel/core": "7.25.2", - "@babel/helper-annotate-as-pure": "7.24.7", - "@babel/helper-split-export-declaration": "7.24.7", - "@babel/plugin-syntax-import-attributes": "7.24.7", - "@inquirer/confirm": "3.1.22", - "@vitejs/plugin-basic-ssl": "1.1.0", - "browserslist": "^4.23.0", - "critters": "0.0.24", - "esbuild": "0.23.0", - "fast-glob": "3.3.2", - "https-proxy-agent": "7.0.5", - "listr2": "8.2.4", - "lmdb": "3.0.13", - "magic-string": "0.30.11", - "mrmime": "2.0.0", - "parse5-html-rewriting-stream": "7.0.0", - "picomatch": "4.0.2", - "piscina": "4.6.1", - "rollup": "4.20.0", - "sass": "1.77.6", - "semver": "7.6.3", - "vite": "5.4.0", - "watchpack": "2.4.1" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "@angular/compiler-cli": "^18.0.0", - "@angular/localize": "^18.0.0", - "@angular/platform-server": "^18.0.0", - "@angular/service-worker": "^18.0.0", - "less": "^4.2.0", - "postcss": "^8.4.0", - "tailwindcss": "^2.0.0 || ^3.0.0", - "typescript": ">=5.4 <5.6" + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" }, - "peerDependenciesMeta": { - "@angular/localize": { - "optional": true - }, - "@angular/platform-server": { - "optional": true - }, - "@angular/service-worker": { - "optional": true - }, - "less": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tailwindcss": { - "optional": true - } + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@angular/cdk": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.1.tgz", - "integrity": "sha512-6y4MmpEPXze6igUHkLsBUPkxw32F8+rmW0xVXZchkSyGlFgqfh53ueXoryWb0qL4s5enkNY6AzXnKAqHfPNkVQ==", + "node_modules/@algolia/ingestion": { + "version": "1.46.2", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.46.2.tgz", + "integrity": "sha512-1Uw2OslTWiOFDtt83y0bGiErJYy5MizadV0nHnOoHFWMoDqWW0kQoMFI65pXqRSkVvit5zjXSLik2xMiyQJDWQ==", + "dev": true, "license": "MIT", "dependencies": { - "tslib": "^2.3.0" + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" }, - "optionalDependencies": { - "parse5": "^7.1.2" - }, - "peerDependencies": { - "@angular/common": "^18.0.0 || ^19.0.0", - "@angular/core": "^18.0.0 || ^19.0.0", - "rxjs": "^6.5.3 || ^7.4.0" + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@angular/cli": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.1.tgz", - "integrity": "sha512-SomUFDHanY4o7k3XBGf1eFt4z1h05IGJHfcbl2vxoc0lY59VN13m/pZsD2AtpqtJTzLQT02XQOUP4rmBbGoQ+Q==", + "node_modules/@algolia/monitoring": { + "version": "1.46.2", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.46.2.tgz", + "integrity": "sha512-xk9f+DPtNcddWN6E7n1hyNNsATBCHIqAvVGG2EAGHJc4AFYL18uM/kMTiOKXE/LKDPyy1JhIerrh9oYb7RBrgw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1802.1", - "@angular-devkit/core": "18.2.1", - "@angular-devkit/schematics": "18.2.1", - "@inquirer/prompts": "5.3.8", - "@listr2/prompt-adapter-inquirer": "2.0.15", - "@schematics/angular": "18.2.1", - "@yarnpkg/lockfile": "1.1.0", - "ini": "4.1.3", - "jsonc-parser": "3.3.1", - "listr2": "8.2.4", - "npm-package-arg": "11.0.3", - "npm-pick-manifest": "9.1.0", - "pacote": "18.0.6", - "resolve": "1.22.8", - "semver": "7.6.3", - "symbol-observable": "4.0.0", - "yargs": "17.7.2" - }, - "bin": { - "ng": "bin/ng.js" + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" + "node": ">= 14.0.0" } }, - "node_modules/@angular/common": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.1.tgz", - "integrity": "sha512-N0ZJO1/iU9UhprplZRPvBcdRgA/i6l6Ng5gXs5ymHBJ0lxsB+mDVCmC4jISjR9gAWc426xXwLaOpuP5Gv3f/yg==", + "node_modules/@algolia/recommend": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.46.2.tgz", + "integrity": "sha512-NApbTPj9LxGzNw4dYnZmj2BoXiAc8NmbbH6qBNzQgXklGklt/xldTvu+FACN6ltFsTzoNU6j2mWNlHQTKGC5+Q==", + "dev": true, "license": "MIT", "dependencies": { - "tslib": "^2.3.0" + "@algolia/client-common": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/core": "18.2.1", - "rxjs": "^6.5.3 || ^7.4.0" + "node": ">= 14.0.0" } }, - "node_modules/@angular/compiler": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.1.tgz", - "integrity": "sha512-5e9ygKEcsBoV6xpaGKVrtsLxLETlrM0oB7twl4qG/xuKYqCLj8cRQMcAKSqDfTPzWMOAQc7pHdk+uFVo/8dWHA==", + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.46.2.tgz", + "integrity": "sha512-ekotpCwpSp033DIIrsTpYlGUCF6momkgupRV/FA3m62SreTSZUKjgK6VTNyG7TtYfq9YFm/pnh65bATP/ZWJEg==", + "dev": true, "license": "MIT", "dependencies": { - "tslib": "^2.3.0" + "@algolia/client-common": "5.46.2" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/core": "18.2.1" - }, - "peerDependenciesMeta": { - "@angular/core": { - "optional": true - } + "node": ">= 14.0.0" } }, - "node_modules/@angular/compiler-cli": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.1.tgz", - "integrity": "sha512-D+Qba0r6RfHfffzrebGYp54h05AxpkagLjit/GczKNgWSP1gIgZxSfi88D+GvFmeWvZxWN1ecAQ+yqft9hJqWg==", + "node_modules/@algolia/requester-fetch": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.46.2.tgz", + "integrity": "sha512-gKE+ZFi/6y7saTr34wS0SqYFDcjHW4Wminv8PDZEi0/mE99+hSrbKgJWxo2ztb5eqGirQTgIh1AMVacGGWM1iw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "7.25.2", - "@jridgewell/sourcemap-codec": "^1.4.14", - "chokidar": "^3.0.0", - "convert-source-map": "^1.5.1", - "reflect-metadata": "^0.2.0", - "semver": "^7.0.0", - "tslib": "^2.3.0", - "yargs": "^17.2.1" - }, - "bin": { - "ng-xi18n": "bundles/src/bin/ng_xi18n.js", - "ngc": "bundles/src/bin/ngc.js", - "ngcc": "bundles/ngcc/index.js" + "@algolia/client-common": "5.46.2" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/compiler": "18.2.1", - "typescript": ">=5.4 <5.6" + "node": ">= 14.0.0" } }, - "node_modules/@angular/core": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.1.tgz", - "integrity": "sha512-9KrSpJ65UlJZNXrE18NszcfOwb5LZgG+LYi5Doe7amt218R1bzb3trvuAm0ZzMaoKh4ugtUCkzEOd4FALPEX6w==", + "node_modules/@algolia/requester-node-http": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.46.2.tgz", + "integrity": "sha512-ciPihkletp7ttweJ8Zt+GukSVLp2ANJHU+9ttiSxsJZThXc4Y2yJ8HGVWesW5jN1zrsZsezN71KrMx/iZsOYpg==", + "dev": true, "license": "MIT", "dependencies": { - "tslib": "^2.3.0" + "@algolia/client-common": "5.46.2" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.14.10" + "node": ">= 14.0.0" } }, - "node_modules/@angular/forms": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.1.tgz", - "integrity": "sha512-T7z8KUuj2PoPxrMrAruQVJha+x4a9Y6IrKYtArgOQQlTwCEJuqpVYuOk5l3fwWpHE9bVEjvgkAMI1D5YXA/U6w==", - "license": "MIT", + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, - "peerDependencies": { - "@angular/common": "18.2.1", - "@angular/core": "18.2.1", - "@angular/platform-browser": "18.2.1", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@angular/language-service": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.1.tgz", - "integrity": "sha512-JI4oox9ELNdDVg0uJqCwgyFoK4XrowV14wSoNpGhpTLModRg3eDS6q+8cKn27cjTQRZvpReyYSTfiZMB8j4eqQ==", - "license": "MIT", "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": ">=6.0.0" } }, - "node_modules/@angular/localize": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.2.1.tgz", - "integrity": "sha512-nNdB6ehXCSBpQ75sTh6Gcwy2rgExfZEkGcPARJLpjqQlHO+Mk3b1y3ka6XT9M2qQYUeyukncTFUMEZWwHICsOA==", + "node_modules/@angular-devkit/architect": { + "version": "0.2101.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2101.3.tgz", + "integrity": "sha512-vKz8aPA62W+e9+pF6ct4CRDG/MjlIH7sWFGYkxPPRst2g46ZQsRkrzfMZAWv/wnt6OZ1OwyRuO3RW83EMhag8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "7.25.2", - "@types/babel__core": "7.20.5", - "fast-glob": "3.3.2", - "yargs": "^17.2.1" + "@angular-devkit/core": "21.1.3", + "rxjs": "7.8.2" }, "bin": { - "localize-extract": "tools/bundles/src/extract/cli.js", - "localize-migrate": "tools/bundles/src/migrate/cli.js", - "localize-translate": "tools/bundles/src/translate/cli.js" + "architect": "bin/cli.js" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/compiler": "18.2.1", - "@angular/compiler-cli": "18.2.1" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@angular/platform-browser": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.1.tgz", - "integrity": "sha512-hQABX7QotGmCIR3EhCBCDh5ZTvQao+JkuK5CCw2G1PkRfJMBwEpjNqnyhz41hZhWiGlucp9jgbeypppW+mIQEw==", + "node_modules/@angular-devkit/architect/node_modules/@angular-devkit/core": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.3.tgz", + "integrity": "sha512-huEXd1tWQHwwN+0VGRT+vSVplV0KNrGFUGJzkIW6iJE1SQElxn6etMai+pSd5DJcePkx6+SuscVsxbfwf70hnA==", + "dev": true, "license": "MIT", "dependencies": { - "tslib": "^2.3.0" + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" }, "peerDependencies": { - "@angular/animations": "18.2.1", - "@angular/common": "18.2.1", - "@angular/core": "18.2.1" + "chokidar": "^5.0.0" }, "peerDependenciesMeta": { - "@angular/animations": { + "chokidar": { "optional": true } } }, - "node_modules/@angular/platform-browser-dynamic": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.1.tgz", - "integrity": "sha512-tYJHtshbaKrtnRA15k3vrveSVBqkVUGhINvGugFA2vMtdTOfhfPw+hhzYrcwJibgU49rHogCfI9mkIbpNRYntA==", + "node_modules/@angular-devkit/architect/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "tslib": "^2.3.0" + "readdirp": "^5.0.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": ">= 20.19.0" }, - "peerDependencies": { - "@angular/common": "18.2.1", - "@angular/compiler": "18.2.1", - "@angular/core": "18.2.1", - "@angular/platform-browser": "18.2.1" + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@angular/router": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.1.tgz", - "integrity": "sha512-gVyqW6fYnG7oq1DlZSXJMQ2Py2dJQB7g6XVtRcYB1gR4aeowx5N9ws7PjqAi0ih91ASq2MmP4OlSSWLq+eaMGg==", + "node_modules/@angular-devkit/architect/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, + "optional": true, + "peer": true, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": ">= 20.19.0" }, - "peerDependencies": { - "@angular/common": "18.2.1", - "@angular/core": "18.2.1", - "@angular/platform-browser": "18.2.1", - "rxjs": "^6.5.3 || ^7.4.0" + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "node_modules/@angular-devkit/architect/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.1.3.tgz", + "integrity": "sha512-Ps7bRl5uOcM7WpNJHbSls/jz5/wAI0ldkTlKyiBFA7RtNeQIABAV+hvlw5DJuEb1Lo5hnK0hXj90AyZdOxzY+w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@angular-devkit/core": "21.1.3", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.21", + "ora": "9.0.0", + "rxjs": "7.8.2" }, "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", - "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.3.tgz", + "integrity": "sha512-huEXd1tWQHwwN+0VGRT+vSVplV0KNrGFUGJzkIW6iJE1SQElxn6etMai+pSd5DJcePkx6+SuscVsxbfwf70hnA==", "dev": true, "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", - "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-module-transforms": "^7.25.2", - "@babel/helpers": "^7.25.0", - "@babel/parser": "^7.25.0", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.2", - "@babel/types": "^7.25.2", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" }, - "engines": { - "node": ">=6.9.0" + "peerDependencies": { + "chokidar": "^5.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/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/@babel/core/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" + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } } }, - "node_modules/@babel/generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", - "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "node_modules/@angular-devkit/schematics/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "@babel/types": "^7.25.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "readdirp": "^5.0.0" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", - "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" + "node": ">= 20.19.0" }, - "engines": { - "node": ">=6.9.0" + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", - "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", + "node_modules/@angular-devkit/schematics/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, + "optional": true, + "peer": true, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", - "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.25.2", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" + "node": ">= 20.19.0" }, - "engines": { - "node": ">=6.9.0" + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/@angular-devkit/schematics/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" } }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz", - "integrity": "sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ==", + "node_modules/@angular-eslint/builder": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.2.0.tgz", + "integrity": "sha512-wcp3J9cbrDwSeI/o1D/DSvMQa8zpKjc5WhRGTx33omhWijCfiVNEAiBLWiEx5Sb/dWcoX8yFNWY5jSgFVy9Sjw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.8", - "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/helper-replace-supers": "^7.25.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/traverse": "^7.25.4", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" + "@angular-devkit/architect": ">= 0.2100.0 < 0.2200.0", + "@angular-devkit/core": ">= 21.0.0 < 22.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/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" + "@angular/cli": ">= 21.0.0 < 22.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" } }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz", - "integrity": "sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g==", + "node_modules/@angular-eslint/builder/node_modules/@angular-devkit/core": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.3.tgz", + "integrity": "sha512-huEXd1tWQHwwN+0VGRT+vSVplV0KNrGFUGJzkIW6iJE1SQElxn6etMai+pSd5DJcePkx6+SuscVsxbfwf70hnA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "regexpu-core": "^5.3.1", - "semver": "^6.3.1" + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" }, "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin/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/@babel/helper-define-polyfill-provider": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", - "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "chokidar": "^5.0.0" }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", - "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", + "node_modules/@angular-eslint/builder/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "@babel/traverse": "^7.24.8", - "@babel/types": "^7.24.8" + "readdirp": "^5.0.0" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "node": ">= 20.19.0" }, - "engines": { - "node": ">=6.9.0" + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", - "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "node_modules/@angular-eslint/builder/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.2" - }, + "optional": true, + "peer": true, "engines": { - "node": ">=6.9.0" + "node": ">= 20.19.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", - "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "node_modules/@angular-eslint/builder/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, + "license": "BSD-3-Clause", "engines": { - "node": ">=6.9.0" + "node": ">= 12" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "node_modules/@angular-eslint/bundled-angular-compiler": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.2.0.tgz", + "integrity": "sha512-J0DWL+j6t9ItFIyIADvzHGqwDA1qfVJ9bx+oTmJ/Hlo7cUpIRoXpcTXpug0CEEABFH0RfDu6PDG2b0FoZ1+7bg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } + "license": "MIT" }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz", - "integrity": "sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw==", + "node_modules/@angular-eslint/eslint-plugin": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.2.0.tgz", + "integrity": "sha512-X2Qn2viDsjm91CEMxNrxDH3qkKpp6un0C1F1BW2p/m9J4AUVfOcXwWz9UpHFSHTRQ+YlTJbiH1ZwwAPeKhFaxA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-wrap-function": "^7.25.0", - "@babel/traverse": "^7.25.0" - }, - "engines": { - "node": ">=6.9.0" + "@angular-eslint/bundled-angular-compiler": "21.2.0", + "@angular-eslint/utils": "21.2.0", + "ts-api-utils": "^2.1.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" } }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz", - "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==", + "node_modules/@angular-eslint/eslint-plugin-template": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.2.0.tgz", + "integrity": "sha512-lJ13Dj0DjR6YiceQR0sRbyWzSzOQ6uZPwK9CJUF3wuZjYAUvL1D61zaU9QrVLtf89NVOxv+dYZHDdu3IDeIqbA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.24.8", - "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/traverse": "^7.25.0" - }, - "engines": { - "node": ">=6.9.0" + "@angular-eslint/bundled-angular-compiler": "21.2.0", + "@angular-eslint/utils": "21.2.0", + "aria-query": "5.3.2", + "axobject-query": "4.1.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@angular-eslint/template-parser": "21.2.0", + "@typescript-eslint/types": "^7.11.0 || ^8.0.0", + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "node_modules/@angular-eslint/schematics": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.2.0.tgz", + "integrity": "sha512-WtT4fPKIUQ/hswy+l2GF/rKOdD+42L3fUzzcwRzNutQbe2tU9SimoSOAsay/ylWEuhIOQTs7ysPB8fUgFQoLpA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@angular-devkit/core": ">= 21.0.0 < 22.0.0", + "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", + "@angular-eslint/eslint-plugin": "21.2.0", + "@angular-eslint/eslint-plugin-template": "21.2.0", + "ignore": "7.0.5", + "semver": "7.7.3", + "strip-json-comments": "3.1.1" }, - "engines": { - "node": ">=6.9.0" + "peerDependencies": { + "@angular/cli": ">= 21.0.0 < 22.0.0" } }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", - "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "node_modules/@angular-eslint/schematics/node_modules/@angular-devkit/core": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.3.tgz", + "integrity": "sha512-huEXd1tWQHwwN+0VGRT+vSVplV0KNrGFUGJzkIW6iJE1SQElxn6etMai+pSd5DJcePkx6+SuscVsxbfwf70hnA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" }, "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } } }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "node_modules/@angular-eslint/schematics/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "@babel/types": "^7.24.7" + "readdirp": "^5.0.0" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "node_modules/@angular-eslint/schematics/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { - "node": ">=6.9.0" + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "node_modules/@angular-eslint/schematics/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "engines": { - "node": ">=6.9.0" + "node": ">= 12" } }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.0.tgz", - "integrity": "sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ==", + "node_modules/@angular-eslint/template-parser": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.2.0.tgz", + "integrity": "sha512-TCb3qYOC/uXKZCo56cJ6N9sHeWdFhyVqrbbYfFjTi09081T6jllgHDZL5Ms7gOMNY8KywWGGbhxwvzeA0RwTgA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.0", - "@babel/types": "^7.25.0" + "@angular-eslint/bundled-angular-compiler": "21.2.0", + "eslint-scope": "^9.0.0" }, - "engines": { - "node": ">=6.9.0" + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" } }, - "node_modules/@babel/helpers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", - "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "node_modules/@angular-eslint/utils": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.2.0.tgz", + "integrity": "sha512-E19/hkuvHoNFvctBkmEiGWpy2bbC6cgbr3GNVrn2nGtbI4jnwnDFCGHv50I4LBfvj0PA9E6TWe73ejJ5qoMJWQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" + "@angular-eslint/bundled-angular-compiler": "21.2.0" }, - "engines": { - "node": ">=6.9.0" + "peerDependencies": { + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" } }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dev": true, + "node_modules/@angular/animations": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.1.3.tgz", + "integrity": "sha512-UADMncDd9lkmIT1NPVFcufyP5gJHMPzxNaQpojiGrxT1aT8Du30mao0KSrB4aTwcicv6/cdD5bZbIyg+FL6LkQ==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "tslib": "^2.3.0" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.4.tgz", - "integrity": "sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.25.4" - }, - "bin": { - "parser": "bin/babel-parser.js" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, - "engines": { - "node": ">=6.0.0" + "peerDependencies": { + "@angular/core": "21.1.3" } }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz", - "integrity": "sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA==", + "node_modules/@angular/build": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.1.3.tgz", + "integrity": "sha512-RXVRuamfrSPwsFCLJgsO2ucp+dwWDbGbhXrQnMrGXm0qdgYpI8bAsBMd8wOeUA6vn4fRmjaRFQZbL/rcIVrkzw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/traverse": "^7.25.3" + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.2101.3", + "@babel/core": "7.28.5", + "@babel/helper-annotate-as-pure": "7.27.3", + "@babel/helper-split-export-declaration": "7.24.7", + "@inquirer/confirm": "5.1.21", + "@vitejs/plugin-basic-ssl": "2.1.0", + "beasties": "0.3.5", + "browserslist": "^4.26.0", + "esbuild": "0.27.2", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "magic-string": "0.30.21", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "8.0.0", + "picomatch": "4.0.3", + "piscina": "5.1.4", + "rolldown": "1.0.0-beta.58", + "sass": "1.97.1", + "semver": "7.7.3", + "source-map-support": "0.5.21", + "tinyglobby": "0.2.15", + "undici": "7.20.0", + "vite": "7.3.0", + "watchpack": "2.5.0" }, "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "optionalDependencies": { + "lmdb": "3.4.4" + }, + "peerDependencies": { + "@angular/compiler": "^21.0.0", + "@angular/compiler-cli": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/localize": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/platform-server": "^21.0.0", + "@angular/service-worker": "^21.0.0", + "@angular/ssr": "^21.1.3", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^21.0.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=5.9 <6.0", + "vitest": "^4.0.8" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@angular/localize": { + "optional": true + }, + "@angular/platform-browser": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + }, + "vitest": { + "optional": true + } } }, - "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.0.tgz", - "integrity": "sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA==", - "dev": true, + "node_modules/@angular/cdk": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.1.3.tgz", + "integrity": "sha512-jMiEKCcZMIAnyx2jxrJHmw5c7JXAiN56ErZ4X+OuQ5yFvYRocRVEs25I0OMxntcXNdPTJQvpGwGlhWhS0yDorg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" - }, - "engines": { - "node": ">=6.9.0" + "parse5": "^8.0.0", + "tslib": "^2.3.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@angular/common": "^21.0.0 || ^22.0.0", + "@angular/core": "^21.0.0 || ^22.0.0", + "@angular/platform-browser": "^21.0.0 || ^22.0.0", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.0.tgz", - "integrity": "sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA==", + "node_modules/@angular/cli": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.1.3.tgz", + "integrity": "sha512-UPtDcpKyrKZRPfym9gTovcibPzl2O/Woy7B8sm45sAnjDH+jDUCcCvuIak7GpH47shQkC2J4yvnHZbD4c6XxcQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@angular-devkit/architect": "0.2101.3", + "@angular-devkit/core": "21.1.3", + "@angular-devkit/schematics": "21.1.3", + "@inquirer/prompts": "7.10.1", + "@listr2/prompt-adapter-inquirer": "3.0.5", + "@modelcontextprotocol/sdk": "1.26.0", + "@schematics/angular": "21.1.3", + "@yarnpkg/lockfile": "1.1.0", + "algoliasearch": "5.46.2", + "ini": "6.0.0", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "npm-package-arg": "13.0.2", + "pacote": "21.0.4", + "parse5-html-rewriting-stream": "8.0.0", + "resolve": "1.22.11", + "semver": "7.7.3", + "yargs": "18.0.0", + "zod": "4.3.5" }, - "engines": { - "node": ">=6.9.0" + "bin": { + "ng": "bin/ng.js" }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", - "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.3.tgz", + "integrity": "sha512-huEXd1tWQHwwN+0VGRT+vSVplV0KNrGFUGJzkIW6iJE1SQElxn6etMai+pSd5DJcePkx6+SuscVsxbfwf70hnA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.7" + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" }, "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" }, "peerDependencies": { - "@babel/core": "^7.13.0" + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } } }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.0.tgz", - "integrity": "sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw==", + "node_modules/@angular/cli/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/traverse": "^7.25.0" - }, "engines": { - "node": ">=6.9.0" + "node": ">=12" }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "node_modules/@angular/cli/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": ">=12" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "node_modules/@angular/cli/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "readdirp": "^5.0.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "node_modules/@angular/cli/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20" } }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "node_modules/@angular/cli/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular/cli/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, + "optional": true, + "peer": true, "engines": { - "node": ">=6.9.0" + "node": ">= 20.19.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "node_modules/@angular/cli/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" } }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "node_modules/@angular/cli/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", - "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "node_modules/@angular/cli/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=6.9.0" + "node": ">=12" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", - "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "node_modules/@angular/cli/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=18" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "node_modules/@angular/cli/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" } }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "node_modules/@angular/cli/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/@angular/common": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.1.3.tgz", + "integrity": "sha512-Wdbln/UqZM5oVnpfIydRdhhL8A9x3bKZ9Zy1/mM0q+qFSftPvmFZIXhEpFqbDwNYbGUhGzx7t8iULC4sVVp/zA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/core": "21.1.3", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, + "node_modules/@angular/compiler": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.1.3.tgz", + "integrity": "sha512-gDNLh7MEf7Qf88ktZzS4LJQXCA5U8aQTfK9ak+0mi2ruZ0x4XSjQCro4H6OPKrrbq94+6GcnlSX5+oVIajEY3w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "tslib": "^2.3.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, + "node_modules/@angular/compiler-cli": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.1.3.tgz", + "integrity": "sha512-nKxoQ89W2B1WdonNQ9kgRnvLNS6DAxDrRHBslsKTlV+kbdv7h59M9PjT4ZZ2sp1M/M8LiofnUfa/s2jd/xYj5w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/core": "7.28.5", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^5.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^18.0.0" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/compiler": "21.1.3", + "typescript": ">=5.9 <6.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, + "node_modules/@angular/compiler-cli/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "engines": { + "node": ">=12" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, + "node_modules/@angular/compiler-cli/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "engines": { + "node": ">=12" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, + "node_modules/@angular/compiler-cli/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "readdirp": "^5.0.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", + "node_modules/@angular/compiler-cli/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20" } }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, + "node_modules/@angular/compiler-cli/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/@angular/compiler-cli/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, "engines": { - "node": ">=6.9.0" + "node": ">= 20.19.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, + "node_modules/@angular/compiler-cli/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=18" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dev": true, + "node_modules/@angular/compiler-cli/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=6.9.0" + "node": ">=12" }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", - "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", - "dev": true, + "node_modules/@angular/compiler-cli/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=18" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.0.tgz", - "integrity": "sha512-uaIi2FdqzjpAMvVqvB51S42oC2JEVgh0LDsGfZVDysWE8LrJtQC2jvKmOqEYThKyB7bDEb7BP1GYWDm7tABA0Q==", - "dev": true, + "node_modules/@angular/compiler-cli/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-remap-async-to-generator": "^7.25.0", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/traverse": "^7.25.0" + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", - "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-remap-async-to-generator": "^7.24.7" - }, + "node_modules/@angular/compiler-cli/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", - "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", - "dev": true, + "node_modules/@angular/core": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.1.3.tgz", + "integrity": "sha512-TbhQxRC7Lb/3WBdm1n8KRsktmVEuGBBp0WRF5mq0Ze4s1YewIM6cULrSw9ACtcL5jdcq7c74ms+uKQsaP/gdcQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "tslib": "^2.3.0" }, "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/compiler": "21.1.3", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0 || ~0.16.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } } }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.0.tgz", - "integrity": "sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ==", - "dev": true, + "node_modules/@angular/forms": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.1.3.tgz", + "integrity": "sha512-YW/YdjM9suZUeJam9agHFXIEE3qQIhGYXMjnnX7xGjOe+CuR2R0qsWn1AR0yrKrNmFspb0lKgM7kTTJyzt8gZg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@standard-schema/spec": "^1.0.0", + "tslib": "^2.3.0" }, "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/common": "21.1.3", + "@angular/core": "21.1.3", + "@angular/platform-browser": "21.1.3", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz", - "integrity": "sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g==", - "dev": true, + "node_modules/@angular/language-service": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-21.1.3.tgz", + "integrity": "sha512-i7iMIMt2rbCDXRuVULbi0I5v4a7ldBgoGdPvHQ17poohTjU4NJ2Jm7p7mUYCGcDlYmWOvgxMGaoiqUs6S5lFPA==", "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.4", - "@babel/helper-plugin-utils": "^7.24.8" - }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", - "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", - "dev": true, + "node_modules/@angular/localize": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-21.1.3.tgz", + "integrity": "sha512-o/zFe8t578OP1j9+7iYibkwcE19zVC8xRFl/+f8bLSwqxwqasMNu1/zCa1B2nq8Gd2xwbvX/7kDhAn25yM4FJg==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-class-static-block": "^7.14.5" + "@babel/core": "7.28.5", + "@types/babel__core": "7.20.5", + "tinyglobby": "^0.2.12", + "yargs": "^18.0.0" + }, + "bin": { + "localize-extract": "tools/bundles/src/extract/cli.js", + "localize-migrate": "tools/bundles/src/migrate/cli.js", + "localize-translate": "tools/bundles/src/translate/cli.js" }, "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.12.0" + "@angular/compiler": "21.1.3", + "@angular/compiler-cli": "21.1.3" } }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.4.tgz", - "integrity": "sha512-oexUfaQle2pF/b6E0dwsxQtAol9TLSO88kQvym6HHBWFliV2lGdrPieX+WgMRLSJDVzdYywk7jXbLPuO2KLTLg==", - "dev": true, + "node_modules/@angular/localize/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-replace-supers": "^7.25.0", - "@babel/traverse": "^7.25.4", - "globals": "^11.1.0" - }, "engines": { - "node": ">=6.9.0" + "node": ">=12" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", - "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", - "dev": true, + "node_modules/@angular/localize/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/template": "^7.24.7" - }, "engines": { - "node": ">=6.9.0" + "node": ">=12" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", - "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", - "dev": true, - "license": "MIT", + "node_modules/@angular/localize/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=20" } }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", - "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", - "dev": true, + "node_modules/@angular/localize/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/@angular/localize/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=18" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", - "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", - "dev": true, + "node_modules/@angular/localize/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=6.9.0" + "node": ">=12" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.0.tgz", - "integrity": "sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g==", - "dev": true, + "node_modules/@angular/localize/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.0", - "@babel/helper-plugin-utils": "^7.24.8" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=18" }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", - "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", - "dev": true, + "node_modules/@angular/localize/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", - "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", - "dev": true, + "node_modules/@angular/localize/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/@angular/material": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-21.1.3.tgz", + "integrity": "sha512-bVjtGSsQYOV6Z2cHCpdQVPVOdDxFKAprGV50BHRlPIIFl0X4hsMquFCMVTMExKP5ABKOzVt8Ae5faaszVcPh3A==", "license": "MIT", "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" + "tslib": "^2.3.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/cdk": "21.1.3", + "@angular/common": "^21.0.0 || ^22.0.0", + "@angular/core": "^21.0.0 || ^22.0.0", + "@angular/forms": "^21.0.0 || ^22.0.0", + "@angular/platform-browser": "^21.0.0 || ^22.0.0", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", - "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", - "dev": true, + "node_modules/@angular/platform-browser": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.1.3.tgz", + "integrity": "sha512-W+ZMXAioaP7CsACafBCHsIxiiKrRTPOlQ+hcC7XNBwy+bn5mjGONoCgLreQs76M8HNWLtr/OAUAr6h26OguOuA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + "tslib": "^2.3.0" }, "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/animations": "21.1.3", + "@angular/common": "21.1.3", + "@angular/core": "21.1.3" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } } }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", - "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", - "dev": true, + "node_modules/@angular/platform-browser-dynamic": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.1.3.tgz", + "integrity": "sha512-wWEjrNtJfxzZmbDWdJSyRau7NWpQ6IFM9QAyn7xH3cQDGCj+Gy9lTU5sUIYQc+7sx3nKWztolc7h/M5meYCTAg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + "tslib": "^2.3.0" }, "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/common": "21.1.3", + "@angular/compiler": "21.1.3", + "@angular/core": "21.1.3", + "@angular/platform-browser": "21.1.3" } }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.25.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz", - "integrity": "sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA==", - "dev": true, + "node_modules/@angular/router": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.1.3.tgz", + "integrity": "sha512-uAw4LAMHXAPCe4SywhlUEWjMYVbbLHwTxLyduSp1b+9aVwep0juy5O/Xttlxd/oigVe0NMnOyJG9y1Br/ubnrg==", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.24.8", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/traverse": "^7.25.1" + "tslib": "^2.3.0" }, "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/common": "21.1.3", + "@angular/core": "21.1.3", + "@angular/platform-browser": "21.1.3", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", - "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", - "dev": true, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-json-strings": "^7.8.3" + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz", - "integrity": "sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw==", - "dev": true, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" - }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", - "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + "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==", + "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" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", - "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", - "dev": true, + "node_modules/@babel/core/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==", + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", - "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", - "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", - "dev": true, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.24.8", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-simple-access": "^7.24.7" + "@babel/compat-data": "^7.28.6", + "@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" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz", - "integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==", - "dev": true, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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==", "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.25.0", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.0" - }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", - "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", - "dev": true, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", - "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", - "dev": true, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1884,847 +1755,413 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-transform-new-target": { + "node_modules/@babel/helper-split-export-declaration": { "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", - "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", - "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", - "dev": true, + "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==", "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", - "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", - "dev": true, + "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==", "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", - "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", - "dev": true, + "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==", "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.24.7" - }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", - "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", - "dev": true, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", - "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", - "dev": true, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + "@babel/types": "^7.29.0" }, - "engines": { - "node": ">=6.9.0" + "bin": { + "parser": "bin/babel-parser.js" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz", - "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==", - "dev": true, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", - "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", - "dev": true, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.4.tgz", - "integrity": "sha512-ao8BG7E2b/URaUQGqN3Tlsg+M3KlHY6rJ1O1gXAEUnZoyNQnvKyH87Kfg+FoxSeyWUB8ISZZsC91C44ZuBFytw==", - "dev": true, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.4", - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", - "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=0.1.90" } }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", - "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", - "dev": true, + "node_modules/@coreui/angular": { + "version": "5.6.12", + "resolved": "https://registry.npmjs.org/@coreui/angular/-/angular-5.6.12.tgz", + "integrity": "sha512-mSAgcNfCiTA0F57mPsoFTECQDGGPpEgAq/Yw6zd5kUHJ8qB91/uXeG7ttNrSxb/SmeEuIM5HFqUH0UY9oAQMXw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" + "@popperjs/core": "~2.11.8", + "tslib": "^2.3.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/animations": "^21.1.0", + "@angular/cdk": "^21.1.0", + "@angular/common": "^21.1.0", + "@angular/core": "^21.1.0", + "@angular/router": "^21.1.0", + "@coreui/coreui": "^5.4.3", + "@coreui/icons-angular": "~5.6.12", + "rxjs": "^7.8.2" } }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", - "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", - "dev": true, + "node_modules/@coreui/angular-chartjs": { + "version": "5.6.12", + "resolved": "https://registry.npmjs.org/@coreui/angular-chartjs/-/angular-chartjs-5.6.12.tgz", + "integrity": "sha512-e/G31gTlwWzgq29sCztCbDb6tCIjzTBqb2laoEi11e7IzF9+xK+ZxnHKfy/cMrhQkkrH/l9m9mvoWWY4YQ15kQ==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "regenerator-transform": "^0.15.2" - }, - "engines": { - "node": ">=6.9.0" + "lodash-es": "^4.17.23", + "tslib": "^2.3.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/core": "^21.1.0", + "@coreui/chartjs": "^4.1.0", + "chart.js": "^4.5.0" } }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", - "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", - "dev": true, + "node_modules/@coreui/chartjs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@coreui/chartjs/-/chartjs-4.2.0.tgz", + "integrity": "sha512-lgAbknvYAnCcZdG5Sv/ZmHF9N9yytAmL1hroBCTd1+1fJNYvDcOmYyj8HNe8nvS9X/yo5bEQ/kx+HenP2gAXFA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@coreui/coreui": "^5.5.0", + "chart.js": "^4.5.1" } }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.7.tgz", - "integrity": "sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw==", - "dev": true, + "node_modules/@coreui/coreui": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@coreui/coreui/-/coreui-5.5.0.tgz", + "integrity": "sha512-ut0LHYck+VHl+sKExaClr3NXl94ihyjUra01ybaPS9YMBVD5IT0lhV4pPBjPyi9xoxL/wBOVc5zzOSliCxBEGw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/coreui" + } + ], "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.1", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" + "html-entities": "^2.6.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@popperjs/core": "^2.11.8" } }, - "node_modules/@babel/plugin-transform-runtime/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/@coreui/icons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@coreui/icons/-/icons-3.0.1.tgz", + "integrity": "sha512-u9UKEcRMyY9pa4jUoLij8pAR03g5g6TLWV33/Mx2ix8sffyi0eO4fLV8DSTQljDCw938zt7KYog5cVKEAJUxxg==", + "license": "MIT" }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", - "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", - "dev": true, + "node_modules/@coreui/icons-angular": { + "version": "5.6.12", + "resolved": "https://registry.npmjs.org/@coreui/icons-angular/-/icons-angular-5.6.12.tgz", + "integrity": "sha512-ZryfsBUpdC02nFk5YppZV9xeQtidILwGLJSmRRCoLD0SLz3tvQhrPycvvIlqYqPAFkA3up9eRFdKDvRO0OMJ7g==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" + "tslib": "^2.3.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/common": "^21.1.0", + "@angular/core": "^21.1.0", + "@angular/platform-browser": "^21.1.0" } }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", - "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", + "node_modules/@coreui/utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@coreui/utils/-/utils-2.0.2.tgz", + "integrity": "sha512-tIFmyKzR96vSD3vqtw4H/4rH/Pctghj+Rp9kWncx1ec2vstC+yphcEUmMk/r+Mm86/Tradi0SIcuCaqvhkyqJA==", + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + "@jridgewell/trace-mapping": "0.3.9" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=12" } }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", - "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", - "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", - "dev": true, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" } }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz", - "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==", + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" } }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", - "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "tslib": "^2.4.0" } }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", - "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "tslib": "^2.4.0" } }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", - "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", + "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", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=18" } }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz", - "integrity": "sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA==", + "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", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.2", - "@babel/helper-plugin-utils": "^7.24.8" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.3.tgz", - "integrity": "sha512-QsYW7UeAaXvLPX9tdVliMJE7MD7M6MLYVTovRTIwhoYQVFHR1rM4wO8wqAezYi3/BpSD+NzVCZ69R6smWiIi8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.25.2", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-validator-option": "^7.24.8", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.3", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.0", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.0", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.0", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.24.7", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.24.7", - "@babel/plugin-transform-async-generator-functions": "^7.25.0", - "@babel/plugin-transform-async-to-generator": "^7.24.7", - "@babel/plugin-transform-block-scoped-functions": "^7.24.7", - "@babel/plugin-transform-block-scoping": "^7.25.0", - "@babel/plugin-transform-class-properties": "^7.24.7", - "@babel/plugin-transform-class-static-block": "^7.24.7", - "@babel/plugin-transform-classes": "^7.25.0", - "@babel/plugin-transform-computed-properties": "^7.24.7", - "@babel/plugin-transform-destructuring": "^7.24.8", - "@babel/plugin-transform-dotall-regex": "^7.24.7", - "@babel/plugin-transform-duplicate-keys": "^7.24.7", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.0", - "@babel/plugin-transform-dynamic-import": "^7.24.7", - "@babel/plugin-transform-exponentiation-operator": "^7.24.7", - "@babel/plugin-transform-export-namespace-from": "^7.24.7", - "@babel/plugin-transform-for-of": "^7.24.7", - "@babel/plugin-transform-function-name": "^7.25.1", - "@babel/plugin-transform-json-strings": "^7.24.7", - "@babel/plugin-transform-literals": "^7.25.2", - "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", - "@babel/plugin-transform-member-expression-literals": "^7.24.7", - "@babel/plugin-transform-modules-amd": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.8", - "@babel/plugin-transform-modules-systemjs": "^7.25.0", - "@babel/plugin-transform-modules-umd": "^7.24.7", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", - "@babel/plugin-transform-new-target": "^7.24.7", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", - "@babel/plugin-transform-numeric-separator": "^7.24.7", - "@babel/plugin-transform-object-rest-spread": "^7.24.7", - "@babel/plugin-transform-object-super": "^7.24.7", - "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.8", - "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.24.7", - "@babel/plugin-transform-private-property-in-object": "^7.24.7", - "@babel/plugin-transform-property-literals": "^7.24.7", - "@babel/plugin-transform-regenerator": "^7.24.7", - "@babel/plugin-transform-reserved-words": "^7.24.7", - "@babel/plugin-transform-shorthand-properties": "^7.24.7", - "@babel/plugin-transform-spread": "^7.24.7", - "@babel/plugin-transform-sticky-regex": "^7.24.7", - "@babel/plugin-transform-template-literals": "^7.24.7", - "@babel/plugin-transform-typeof-symbol": "^7.24.8", - "@babel/plugin-transform-unicode-escapes": "^7.24.7", - "@babel/plugin-transform-unicode-property-regex": "^7.24.7", - "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.4", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.37.1", - "semver": "^6.3.1" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env/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": ">=18" } }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "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", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/runtime": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", - "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", + "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", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "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", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/traverse": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.4.tgz", - "integrity": "sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==", + "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", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.4", - "@babel/parser": "^7.25.4", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.4", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.25.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.5.tgz", - "integrity": "sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==", + "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", - "dependencies": { - "@babel/types": "^7.25.4", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/types": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.4.tgz", - "integrity": "sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@coreui/angular": { - "version": "5.2.16", - "resolved": "https://registry.npmjs.org/@coreui/angular/-/angular-5.2.16.tgz", - "integrity": "sha512-/z6Lyh9JgH4lqq0OpshOPkmmeFXx/9nLZZhtaLRKO0Z9sJcw+wp1bNJLiq+wNy+QMYc1Nuz4CRJJgFEG1MwuLw==", - "license": "MIT", - "dependencies": { - "@popperjs/core": "~2.11.8", - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/animations": "^18.2.0", - "@angular/cdk": "^18.2.0", - "@angular/common": "^18.2.0", - "@angular/core": "^18.2.0", - "@angular/router": "^18.2.0", - "@coreui/coreui": "^5.1.2", - "@coreui/icons-angular": "~5.2.16", - "rxjs": "^7.8.1" - } - }, - "node_modules/@coreui/angular-chartjs": { - "version": "5.2.16", - "resolved": "https://registry.npmjs.org/@coreui/angular-chartjs/-/angular-chartjs-5.2.16.tgz", - "integrity": "sha512-EK4rYzzFM6H48JFZwf4Uizsn9BKL2diUYQqs8IeIaRHZDZ5wqsBncrPZDXwznFieNs/Wf4q3lPddTGEO6DX+1A==", - "license": "MIT", - "dependencies": { - "lodash-es": "^4.17.21", - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/core": "^18.2.0", - "@coreui/chartjs": "^4.0.0", - "chart.js": "^4.4.4" - } - }, - "node_modules/@coreui/chartjs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@coreui/chartjs/-/chartjs-4.0.0.tgz", - "integrity": "sha512-gPxmqj6hpC/erZBfyKQ+axWKr1gY4yhj8Dm3WkBp8SG2lUs0lEAQy3XGmmM/42TBTylbq5V4P6jfqim3N0mKmw==", - "license": "MIT", - "dependencies": { - "@coreui/coreui": "^5.0.0", - "chart.js": "^4.4.2" - } - }, - "node_modules/@coreui/coreui": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@coreui/coreui/-/coreui-5.1.2.tgz", - "integrity": "sha512-ub7KfpLo5OomUQ2Ah6rvSgmX+JzJmJ7XfU951BYpcpveCdAy1GWaKhH83JGVqcKQyHVBe+z8Y5Z5s19WmWzilw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/coreui" - } - ], - "license": "MIT", - "peerDependencies": { - "@popperjs/core": "^2.11.8" - } - }, - "node_modules/@coreui/icons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@coreui/icons/-/icons-3.0.1.tgz", - "integrity": "sha512-u9UKEcRMyY9pa4jUoLij8pAR03g5g6TLWV33/Mx2ix8sffyi0eO4fLV8DSTQljDCw938zt7KYog5cVKEAJUxxg==", - "license": "MIT" - }, - "node_modules/@coreui/icons-angular": { - "version": "5.2.16", - "resolved": "https://registry.npmjs.org/@coreui/icons-angular/-/icons-angular-5.2.16.tgz", - "integrity": "sha512-btPzRyCLj1cC1KJbo09NLYfYDcjBEaou3IyOFTrgVDqEJ9t2Ff5lz00ew9gSrQg/512rMfvL3/qS6P+1cQ/epg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/common": "^18.2.0", - "@angular/core": "^18.2.0", - "@angular/platform-browser": "^18.2.0" - } - }, - "node_modules/@coreui/utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@coreui/utils/-/utils-2.0.2.tgz", - "integrity": "sha512-tIFmyKzR96vSD3vqtw4H/4rH/Pctghj+Rp9kWncx1ec2vstC+yphcEUmMk/r+Mm86/Tradi0SIcuCaqvhkyqJA==", - "license": "MIT" - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.1.tgz", - "integrity": "sha512-boghen8F0Q8D+0/Q1/1r6DUEieUJ8w2a1gIknExMSHBsJFOr2+0KUfHiVYBvucPwl3+RU5PFBK833FjFCh3BhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.17.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", - "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", - "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", - "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", - "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", - "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", - "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", - "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", - "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", - "cpu": [ - "x64" - ], + "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, @@ -2736,9 +2173,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", - "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "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" ], @@ -2753,9 +2190,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", - "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], @@ -2770,9 +2207,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", - "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "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" ], @@ -2787,9 +2224,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", - "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "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" ], @@ -2804,9 +2241,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", - "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "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" ], @@ -2821,9 +2258,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", - "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], @@ -2838,9 +2275,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", - "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], @@ -2855,9 +2292,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", - "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], @@ -2872,9 +2309,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", - "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "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" ], @@ -2888,10 +2325,27 @@ "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.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", - "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "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" ], @@ -2906,9 +2360,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", - "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "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" ], @@ -2923,9 +2377,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", - "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "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" ], @@ -2939,10 +2393,27 @@ "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.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", - "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], @@ -2957,9 +2428,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", - "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "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" ], @@ -2974,9 +2445,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", - "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "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" ], @@ -2991,9 +2462,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", - "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "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" ], @@ -3007,506 +2478,772 @@ "node": ">=18" } }, - "node_modules/@inquirer/checkbox": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.4.7.tgz", - "integrity": "sha512-5YwCySyV1UEgqzz34gNsC38eKxRBtlRDpJLlKcRtTjlYA/yDKuc1rfw+hjw+2WJxbAZtaDPsRl5Zk7J14SBoBw==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", - "ansi-escapes": "^4.3.2", - "yoctocolors-cjs": "^2.1.2" + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": ">=18" + "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/@inquirer/confirm": { - "version": "3.1.22", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.22.tgz", - "integrity": "sha512-gsAKIOWBm2Q87CDfs9fEo7wJT3fwWIJfnDGMn9Qy74gBnNFOACDNfhUzovubbJjWnKLGBln7/NcSmZwj5DuEXg==", + "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", - "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2" - }, "engines": { - "node": ">=18" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@inquirer/core": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.0.10.tgz", - "integrity": "sha512-TdESOKSVwf6+YWDz8GhS6nKscwzkIyakEzCLJ5Vh6O3Co2ClhCJ0A4MG909MUWfaWdpJm7DE45ii51/2Kat9tA==", + "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": "MIT", + "license": "Apache-2.0", "dependencies": { - "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", - "@types/mute-stream": "^0.0.4", - "@types/node": "^22.1.0", - "@types/wrap-ansi": "^3.0.0", - "ansi-escapes": "^4.3.2", - "cli-spinners": "^2.9.2", - "cli-width": "^4.1.0", - "mute-stream": "^1.0.0", - "signal-exit": "^4.1.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@inquirer/core/node_modules/@types/node": { - "version": "22.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", - "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", + "node_modules/@eslint/config-array/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": { - "undici-types": "~6.19.2" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/@inquirer/editor": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-2.1.22.tgz", - "integrity": "sha512-K1QwTu7GCK+nKOVRBp5HY9jt3DXOfPGPr6WRDrPImkcJRelG9UTx2cAtK1liXmibRrzJlTWOwqgWT3k2XnS62w==", + "node_modules/@eslint/config-array/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": "MIT", + "license": "ISC", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", - "external-editor": "^3.1.0" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=18" + "node": "*" } }, - "node_modules/@inquirer/expand": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-2.1.22.tgz", - "integrity": "sha512-wTZOBkzH+ItPuZ3ZPa9lynBsdMp6kQ9zbjVPYEtSBG7UulGjg2kQiAnUjgyG4SlntpTce5bOmXAPvE4sguXjpA==", + "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": "MIT", + "license": "Apache-2.0", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", - "yoctocolors-cjs": "^2.1.2" + "@eslint/core": "^0.17.0" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/figures": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.5.tgz", - "integrity": "sha512-79hP/VWdZ2UVc9bFGJnoQ/lQMpL74mGgzSYX1xUqCVk7/v73vJCMw1VuyWN1jGkZ9B3z7THAbySqGbCNefcjfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@inquirer/input": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.2.9.tgz", - "integrity": "sha512-7Z6N+uzkWM7+xsE+3rJdhdG/+mQgejOVqspoW+w0AbSZnL6nq5tGMEVASaYVWbkoSzecABWwmludO2evU3d31g==", + "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": "MIT", + "license": "Apache-2.0", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2" + "@types/json-schema": "^7.0.15" }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@inquirer/number": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-1.0.10.tgz", - "integrity": "sha512-kWTxRF8zHjQOn2TJs+XttLioBih6bdc5CcosXIzZsrTY383PXI35DuhIllZKu7CdXFi2rz2BWPN9l0dPsvrQOA==", + "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": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2" + "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" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@inquirer/password": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-2.1.22.tgz", - "integrity": "sha512-5Fxt1L9vh3rAKqjYwqsjU4DZsEvY/2Gll+QkqR4yEpy6wvzLxdSgFhUcxfDAOtO4BEoTreWoznC0phagwLU5Kw==", + "node_modules/@eslint/eslintrc/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": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", - "ansi-escapes": "^4.3.2" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, - "engines": { - "node": ">=18" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@inquirer/prompts": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-5.3.8.tgz", - "integrity": "sha512-b2BudQY/Si4Y2a0PdZZL6BeJtl8llgeZa7U2j47aaJSCeAl1e4UI7y8a9bSkO3o/ZbZrgT5muy/34JbsjfIWxA==", + "node_modules/@eslint/eslintrc/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": { - "@inquirer/checkbox": "^2.4.7", - "@inquirer/confirm": "^3.1.22", - "@inquirer/editor": "^2.1.22", - "@inquirer/expand": "^2.1.22", - "@inquirer/input": "^2.2.9", - "@inquirer/number": "^1.0.10", - "@inquirer/password": "^2.1.22", - "@inquirer/rawlist": "^2.2.4", - "@inquirer/search": "^1.0.7", - "@inquirer/select": "^2.4.7" - }, - "engines": { - "node": ">=18" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/@inquirer/rawlist": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-2.2.4.tgz", - "integrity": "sha512-pb6w9pWrm7EfnYDgQObOurh2d2YH07+eDo3xQBsNAM2GRhliz6wFXGi1thKQ4bN6B0xDd6C3tBsjdr3obsCl3Q==", + "node_modules/@eslint/eslintrc/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/@eslint/eslintrc/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/@eslint/eslintrc/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": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", - "yoctocolors-cjs": "^2.1.2" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=18" + "node": "*" } }, - "node_modules/@inquirer/search": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-1.0.7.tgz", - "integrity": "sha512-p1wpV+3gd1eST/o5N3yQpYEdFNCzSP0Klrl+5bfD3cTTz8BGG6nf4Z07aBW0xjlKIj1Rp0y3x/X4cZYi6TfcLw==", + "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", - "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", - "yoctocolors-cjs": "^2.1.2" + "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" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@inquirer/select": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.4.7.tgz", - "integrity": "sha512-JH7XqPEkBpNWp3gPCqWqY8ECbyMoFcCZANlL6pV9hf59qK6dGmkOlx1ydyhY+KZ0c5X74+W6Mtp+nm2QX0/MAQ==", + "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": "MIT", + "license": "Apache-2.0", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", - "ansi-escapes": "^4.3.2", - "yoctocolors-cjs": "^2.1.2" + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@inquirer/type": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.2.tgz", - "integrity": "sha512-w9qFkumYDCNyDZmNQjf/n6qQuvQ4dMC3BJesY4oF+yr0CxR5vxujflAVeIcS6U336uzi9GM0kAfZlLrZ9UTkpA==", + "node_modules/@f123dashboard/shared": { + "resolved": "shared", + "link": true + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", "dev": true, "license": "MIT", - "dependencies": { - "mute-stream": "^1.0.0" + "engines": { + "node": ">=18.14.1" }, + "peerDependencies": { + "hono": "^4" + } + }, + "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" + "node": ">=18.18.0" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "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": "ISC", + "license": "Apache-2.0", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" }, "engines": { - "node": ">=12" + "node": ">=18.18.0" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "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": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=12.22" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "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": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=18.18" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", "dev": true, "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" }, "engines": { - "node": ">=12" + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": ">=12" + "node": ">=18" }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" }, "engines": { - "node": ">=12" + "node": ">=18" }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": ">=6.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "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==", + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", "dev": true, "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, "engines": { - "node": ">=6.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", "dev": true, "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, "engines": { - "node": ">=6.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" + "engines": { + "node": ">=18" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jsonjoy.com/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "engines": { + "node": ">=18" }, "peerDependencies": { - "tslib": "2" + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@jsonjoy.com/json-pack": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.1.0.tgz", - "integrity": "sha512-zlQONA+msXPPwHWZMKFVS78ewFczIll5lXiVPwFPCZUsrOKdxc2AvxU1HoNBmMRhqDZUR9HkC3UOm+6pME6Xsg==", + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jsonjoy.com/base64": "^1.1.1", - "@jsonjoy.com/util": "^1.1.2", - "hyperdyperid": "^1.2.0", - "thingies": "^1.20.0" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "node": ">=18" }, "peerDependencies": { - "tslib": "2" + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@jsonjoy.com/util": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.3.0.tgz", - "integrity": "sha512-Cebt4Vk7k1xHy87kHY7KSPLT77A7Ev7IfOblyLZhtYEhrdQ6fX4EoLq3xOQ3O/DRMEh2ok5nyC180E+ABS8Wmw==", + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "engines": { + "node": ">=18" }, "peerDependencies": { - "tslib": "2" + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@kurkle/color": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", - "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==", - "license": "MIT" - }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@listr2/prompt-adapter-inquirer": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.15.tgz", - "integrity": "sha512-MZrGem/Ujjd4cPTLYDfCZK2iKKeiO/8OX13S6jqxldLs0Prf2aGqVlJ77nMBqMv7fzqgXEgjrNHLXcKR8l9lOg==", + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/type": "^1.5.1" + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=18" }, "peerDependencies": { - "@inquirer/prompts": ">= 3 < 6" - } - }, - "node_modules/@lmdb/lmdb-darwin-arm64": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.0.13.tgz", - "integrity": "sha512-uiKPB0Fv6WEEOZjruu9a6wnW/8jrjzlZbxXscMB8kuCJ1k6kHpcBnuvaAWcqhbI7rqX5GKziwWEdD+wi2gNLfA==", - "cpu": [ + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", + "integrity": "sha512-WELs+hj6xcilkloBXYf9XXK8tYEnKsgLj01Xl5ONUJpKjmT5hGVUzNUS5tooUxs7pGMrw+jFD/41WpqW4V3LDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8", + "listr2": "9.0.5" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.4.4.tgz", + "integrity": "sha512-XaKL705gDWd6XVls3ATDj13ZdML/LqSIxwgnYpG8xTzH2ifArx8fMMDdvqGE/Emd+W6R90W2fveZcJ0AyS8Y0w==", + "cpu": [ "arm64" ], "dev": true, @@ -3517,9 +3254,9 @@ ] }, "node_modules/@lmdb/lmdb-darwin-x64": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.0.13.tgz", - "integrity": "sha512-bEVIIfK5mSQoG1R19qA+fJOvCB+0wVGGnXHT3smchBVahYBdlPn2OsZZKzlHWfb1E+PhLBmYfqB5zQXFP7hJig==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.4.4.tgz", + "integrity": "sha512-GPHGEVcwJlkD01GmIr7B4kvbIcUDS2+kBadVEd7lU4can1RZaZQLDDBJRrrNfS2Kavvl0VLI/cMv7UASAXGrww==", "cpu": [ "x64" ], @@ -3531,9 +3268,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-arm": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.0.13.tgz", - "integrity": "sha512-Yml1KlMzOnXj/tnW7yX8U78iAzTk39aILYvCPbqeewAq1kSzl+w59k/fiVkTBfvDi/oW/5YRxL+Fq+Y1Fr1r2Q==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.4.4.tgz", + "integrity": "sha512-cmev5/dZr5ACKri9f6GU6lZCXTjMhV72xujlbOhFCgFXrt4W0TxGsmY8kA1BITvH60JBKE50cSxsiulybAbrrw==", "cpu": [ "arm" ], @@ -3545,9 +3282,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-arm64": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.0.13.tgz", - "integrity": "sha512-afbVrsMgZ9dUTNUchFpj5VkmJRxvht/u335jUJ7o23YTbNbnpmXif3VKQGCtnjSh+CZaqm6N3CPG8KO3zwyZ1Q==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.4.4.tgz", + "integrity": "sha512-mALqr7DE42HsiwVTKpQWxacjHoJk+e9p00RWIJqTACh/hpucxp/0lK/XMh5XzWnU/TDCZLukq1+vNqnNumTP/Q==", "cpu": [ "arm64" ], @@ -3559,9 +3296,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-x64": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.0.13.tgz", - "integrity": "sha512-vOtxu0xC0SLdQ2WRXg8Qgd8T32ak4SPqk5zjItRszrJk2BdeXqfGxBJbP7o4aOvSPSmSSv46Lr1EP4HXU8v7Kg==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.4.4.tgz", + "integrity": "sha512-QjLs8OcmCNcraAcLoZyFlo0atzBJniQLLwhtR+ymQqS5kLYpV5RqwriL87BW+ZiR9ZiGgZx3evrz5vnWPtJ1fQ==", "cpu": [ "x64" ], @@ -3572,10 +3309,24 @@ "linux" ] }, + "node_modules/@lmdb/lmdb-win32-arm64": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.4.4.tgz", + "integrity": "sha512-tr/pwHDlZ33forLGAr0tI04cRmP4SgF93yHbb+2zvZiDEyln5yMHhbKDySxY66aUOkhvBvTuHq9q/3YmTj6ZHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@lmdb/lmdb-win32-x64": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.0.13.tgz", - "integrity": "sha512-UCrMJQY/gJnOl3XgbWRZZUvGGBuKy6i0YNSptgMzHBjs+QYDYR1Mt/RLTOPy4fzzves65O1EDmlL//OzEqoLlA==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.4.4.tgz", + "integrity": "sha512-KRzfocJzB/mgoTCqnMawuLSKheHRVTqWfSmouIgYpFs6Hx4zvZSvsZKSCEb5gHmICy7qsx9l06jk3MFTtiFVAQ==", "cpu": [ "x64" ], @@ -3586,6 +3337,47 @@ "win32" ] }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", @@ -3670,140 +3462,460 @@ "win32" ] }, - "node_modules/@ngtools/webpack": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.1.tgz", - "integrity": "sha512-v86U3jOoy5R9ZWe9Q0LbHRx/IBw1lbn0ldBU+gIIepREyVvb9CcH/vAyIb2Fw1zaYvvfG1OyzdrHyW8iGXjdnQ==", + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", "dev": true, "license": "MIT", + "optional": true, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" + "node": ">= 10" }, - "peerDependencies": { - "@angular/compiler-cli": "^18.0.0", - "typescript": ">=5.4 <5.6", - "webpack": "^5.54.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" } }, - "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==", + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 8" + "node": ">= 10" } }, - "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==", + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 8" + "node": ">= 10" } }, - "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==", + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 8" + "node": ">= 10" } }, - "node_modules/@npmcli/agent": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", - "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">= 10" } }, - "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@ng-bootstrap/ng-bootstrap": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-20.0.0.tgz", + "integrity": "sha512-Jt+GUQ0PdM8VsOUUVr7vTQXhwcGwe2DCe1mmfS21vz9pLSOtGRz41ohZKc1egUevj5Rxm2sHVq5Sve68/nTMfA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/forms": "^21.0.0", + "@angular/localize": "^21.0.0", + "@popperjs/core": "^2.11.8", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@npmcli/agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/@npmcli/fs": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", - "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", "dev": true, "license": "ISC", "dependencies": { "semver": "^7.3.5" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/git": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.8.tgz", - "integrity": "sha512-liASfw5cqhjNW9UFd+ruwwdEf/lbOAQjLL2XY2dFW/bkJheXDYZgOyul/4gVvEV4BWkTXjYGmDqMw9uegdbJNQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.1.tgz", + "integrity": "sha512-+XTFxK2jJF/EJJ5SoAzXk3qwIDfvFc5/g+bD274LZ7uY7LE8sTfG6Z8rOanPl2ZEvZWqNvmEdtXC25cE54VcoA==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/promise-spawn": "^7.0.0", - "ini": "^4.1.3", - "lru-cache": "^10.0.1", - "npm-pick-manifest": "^9.0.0", - "proc-log": "^4.0.0", - "promise-inflight": "^1.0.1", + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", "promise-retry": "^2.0.1", "semver": "^7.3.5", - "which": "^4.0.0" + "which": "^6.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/git/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.2.tgz", + "integrity": "sha512-mIcis6w+JiQf3P7t7mg/35GKB4T1FQsBOtMIvuKw4YErj5RjtbhcTd5/I30fmkmGMwvI0WlzSNN+27K0QCMkAw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=20" } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/@npmcli/git/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", "dev": true, "license": "ISC", "dependencies": { @@ -3813,129 +3925,116 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/installed-package-contents": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", - "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", + "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", "dev": true, "license": "ISC", "dependencies": { - "npm-bundled": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" }, "bin": { "installed-package-contents": "bin/index.js" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/node-gyp": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", - "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", + "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", "dev": true, "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/package-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.2.0.tgz", - "integrity": "sha512-qe/kiqqkW0AGtvBjL8TJKZk/eBBSpnJkUWvHdQ9jM2lKHXRYYJuyNpJPlJw3c8QjC2ow6NZYiLExhUaeJelbxQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.4.tgz", + "integrity": "sha512-0wInJG3j/K40OJt/33ax47WfWMzZTm6OQxB9cDhTt5huCP2a9g2GnlsxmfN+PulItNPIpPrZ+kfwwUil7eHcZQ==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/git": "^5.0.0", - "glob": "^10.2.2", - "hosted-git-info": "^7.0.0", - "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^6.0.0", - "proc-log": "^4.0.0", - "semver": "^7.5.3" + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/package-json/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/package-json/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", + "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", + "minimatch": "^10.1.2", "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "path-scurry": "^2.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@npmcli/package-json/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.1" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@npmcli/promise-spawn": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", - "integrity": "sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", "dev": true, "license": "ISC", "dependencies": { - "which": "^4.0.0" + "which": "^6.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/promise-spawn/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.2.tgz", + "integrity": "sha512-mIcis6w+JiQf3P7t7mg/35GKB4T1FQsBOtMIvuKw4YErj5RjtbhcTd5/I30fmkmGMwvI0WlzSNN+27K0QCMkAw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=20" } }, "node_modules/@npmcli/promise-spawn/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", "dev": true, "license": "ISC", "dependencies": { @@ -3945,51 +4044,51 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/redact": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-2.0.1.tgz", - "integrity": "sha512-YgsR5jCQZhVmTJvjduTOIHph0L73pK8xwMVaDY0PatySqVM9AZj93jpoXYSJqfHFxFkN9dmqTw6OiqExsS3LPw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", "dev": true, "license": "ISC", "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/run-script": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-8.1.0.tgz", - "integrity": "sha512-y7efHHwghQfk28G2z3tlZ67pLG0XdfYbcVG26r7YIXALRsrVQcTq4/tdenSmdOrEsNahIYA/eh8aEVROWGFUDg==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.3.tgz", + "integrity": "sha512-ER2N6itRkzWbbtVmZ9WKaWxVlKlOeBFF1/7xx+KA5J1xKa4JjUwBdb6tDpk0v1qA+d+VDwHI9qmLcXSWcmi+Rw==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/node-gyp": "^3.0.0", - "@npmcli/package-json": "^5.0.0", - "@npmcli/promise-spawn": "^7.0.0", - "node-gyp": "^10.0.0", - "proc-log": "^4.0.0", - "which": "^4.0.0" + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0", + "which": "^6.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/run-script/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.2.tgz", + "integrity": "sha512-mIcis6w+JiQf3P7t7mg/35GKB4T1FQsBOtMIvuKw4YErj5RjtbhcTd5/I30fmkmGMwvI0WlzSNN+27K0QCMkAw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=20" } }, "node_modules/@npmcli/run-script/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", "dev": true, "license": "ISC", "dependencies": { @@ -3999,48 +4098,70 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@oxc-project/types": { + "version": "0.106.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.106.0.tgz", + "integrity": "sha512-QdsH3rZq480VnOHSHgPYOhjL8O8LBdcnSjM408BpPCCUc0JYYZPG9Gafl9i3OcGk/7137o+gweb4cCv3WAUykg==", "dev": true, "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" + "dependencies": { + "@noble/hashes": "^1.1.5" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz", - "integrity": "sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==", - "cpu": [ - "arm" - ], + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "dev": true, + "hasInstallScript": true, "license": "MIT", "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.20.0.tgz", - "integrity": "sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", "cpu": [ "arm64" ], @@ -4049,12 +4170,19 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz", - "integrity": "sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==", + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", "cpu": [ "arm64" ], @@ -4063,12 +4191,19 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.20.0.tgz", - "integrity": "sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==", + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", "cpu": [ "x64" ], @@ -4077,26 +4212,40 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.20.0.tgz", - "integrity": "sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==", + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", "cpu": [ - "arm" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.20.0.tgz", - "integrity": "sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==", + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", "cpu": [ "arm" ], @@ -4105,26 +4254,40 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.20.0.tgz", - "integrity": "sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==", + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.20.0.tgz", - "integrity": "sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==", + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", "cpu": [ "arm64" ], @@ -4133,54 +4296,40 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.20.0.tgz", - "integrity": "sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==", - "cpu": [ - "ppc64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.20.0.tgz", - "integrity": "sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==", + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.20.0.tgz", - "integrity": "sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==", - "cpu": [ - "s390x" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz", - "integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==", + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", "cpu": [ "x64" ], @@ -4189,12 +4338,19 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz", - "integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==", + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", "cpu": [ "x64" ], @@ -4203,12 +4359,19 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.20.0.tgz", - "integrity": "sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==", + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], @@ -4217,12 +4380,19 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.20.0.tgz", - "integrity": "sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==", + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ "ia32" ], @@ -4231,12 +4401,19 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.20.0.tgz", - "integrity": "sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==", + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "cpu": [ "x64" ], @@ -4245,2286 +4422,2362 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@schematics/angular": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.1.tgz", - "integrity": "sha512-bBV7I+MCbdQmBPUFF4ECg37VReM0+AdQsxgwkjBBSYExmkErkDoDgKquwL/tH7stDCc5IfTd0g9BMeosRgDMug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "18.2.1", - "@angular-devkit/schematics": "18.2.1", - "jsonc-parser": "3.3.1" - }, + ], "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@sigstore/bundle": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.2.tgz", - "integrity": "sha512-wueKWDk70QixNLB363yHc2D2ItTgYiMTdPwK8D9dKQMR3ZQ0c35IxP5xnwQ8cNLoCgCRcHf14kE+CLIvNX1zmA==", + "node_modules/@playwright/test": { + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.1.tgz", + "integrity": "sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.3.2" + "playwright": "1.58.1" + }, + "bin": { + "playwright": "cli.js" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">=18" } }, - "node_modules/@sigstore/core": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-1.1.0.tgz", - "integrity": "sha512-JzBqdVIyqm2FRQCulY6nbQzMpJJpSiJ8XXWMhtOX9eKgaXXpfNOF53lzQEjIydlStnd/eFtuC1dW4VYdD93oRg==", + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.58.tgz", + "integrity": "sha512-mWj5eE4Qc8TbPdGGaaLvBb9XfDPvE1EmZkJQgiGKwchkWH4oAJcRAKMTw7ZHnb1L+t7Ah41sBkAecaIsuUgsug==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@sigstore/protobuf-specs": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.2.tgz", - "integrity": "sha512-c6B0ehIWxMI8wiS/bj6rHMPqeFvngFV7cDU/MY+B16P9Z3Mp9k8L93eYZ7BYzSickzuqAQqAq0V956b3Ju6mLw==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.58.tgz", + "integrity": "sha512-wFxUymI/5R8bH8qZFYDfAxAN9CyISEIYke+95oZPiv6EWo88aa5rskjVcCpKA532R+klFmdqjbbaD56GNmTF4Q==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@sigstore/sign": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.2.tgz", - "integrity": "sha512-5Vz5dPVuunIIvC5vBb0APwo7qKA4G9yM48kPWJT+OEERs40md5GoUR1yedwpekWZ4m0Hhw44m6zU+ObsON+iDA==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.58.tgz", + "integrity": "sha512-ybp3MkPj23VDV9PhtRwdU5qrGhlViWRV5BjKwO6epaSlUD5lW0WyY+roN3ZAzbma/9RrMTgZ/a/gtQq8YXOcqw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^2.3.2", - "@sigstore/core": "^1.0.0", - "@sigstore/protobuf-specs": "^0.3.2", - "make-fetch-happen": "^13.0.1", - "proc-log": "^4.2.0", - "promise-retry": "^2.0.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@sigstore/tuf": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.4.tgz", - "integrity": "sha512-44vtsveTPUpqhm9NCrbU8CWLe3Vck2HO1PNLw7RIajbB7xhtn5RBPm1VNSCMwqGYHhDsBJG8gDF0q4lgydsJvw==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.58.tgz", + "integrity": "sha512-Evxj3yh7FWvyklUYZa0qTVT9N2zX9TPDqGF056hl8hlCZ9/ndQ2xMv6uw9PD1VlLpukbsqL+/C6M0qwipL0QMg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.3.2", - "tuf-js": "^2.2.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@sigstore/verify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.2.1.tgz", - "integrity": "sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.58.tgz", + "integrity": "sha512-tYeXprDOrEgVHUbPXH6MPso4cM/c6RTkmJNICMQlYdki4hGMh92aj3yU6CKs+4X5gfG0yj5kVUw/L4M685SYag==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^2.3.2", - "@sigstore/core": "^1.1.0", - "@sigstore/protobuf-specs": "^0.3.2" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@sindresorhus/merge-streams": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", - "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.58.tgz", + "integrity": "sha512-N78vmZzP6zG967Ohr+MasCjmKtis0geZ1SOVmxrA0/bklTQSzH5kHEjW5Qn+i1taFno6GEre1E40v0wuWsNOQw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tufjs/canonical-json": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", - "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.58.tgz", + "integrity": "sha512-l+p4QVtG72C7wI2SIkNQw/KQtSjuYwS3rV6AKcWrRBF62ClsFUcif5vLaZIEbPrCXu5OFRXigXFJnxYsVVZqdQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@tufjs/models": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.1.tgz", - "integrity": "sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.58.tgz", + "integrity": "sha512-urzJX0HrXxIh0FfxwWRjfPCMeInU9qsImLQxHBgLp5ivji1EEUnOfux8KxPPnRQthJyneBrN2LeqUix9DYrNaQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.4" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@tufjs/models/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.58.tgz", + "integrity": "sha512-7ijfVK3GISnXIwq/1FZo+KyAUJjL3kWPJ7rViAL6MWeEBhEgRzJ0yEd9I8N9aut8Y8ab+EKFJyRNMWZuUBwQ0A==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.58.tgz", + "integrity": "sha512-/m7sKZCS+cUULbzyJTIlv8JbjNohxbpAOA6cM+lgWgqVzPee3U6jpwydrib328JFN/gF9A99IZEnuGYqEDJdww==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^20.19.0 || >=22.12.0" } }, - "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==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.58.tgz", + "integrity": "sha512-6SZk7zMgv+y3wFFQ9qE5P9NnRHcRsptL1ypmudD26PDY+PvFCvfHRkJNfclWnvacVGxjowr7JOL3a9fd1wWhUw==", + "cpu": [ + "wasm32" + ], "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.58.tgz", + "integrity": "sha512-sFqfYPnBZ6xBhMkadB7UD0yjEDRvs7ipR3nCggblN+N4ODCXY6qhg/bKL39+W+dgQybL7ErD4EGERVbW9DAWvg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.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==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.58.tgz", + "integrity": "sha512-AnFWJdAqB8+IDPcGrATYs67Kik/6tnndNJV2jGRmwlbeNiQQ8GhRJU8ETRlINfII0pqi9k4WWLnb00p1QCxw/Q==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.58.tgz", + "integrity": "sha512-qWhDs6yFGR5xDfdrwiSa3CWGIHxD597uGE/A9xGqytBjANvh4rLCTTkq7szhMV4+Ygh+PMS90KVJ8xWG/TkX4w==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.20.7" - } + "license": "MIT" }, - "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@types/bonjour": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", - "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", - "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@types/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@types/eslint": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.0.tgz", - "integrity": "sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.5", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", - "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true, - "license": "MIT" + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/http-proxy": { - "version": "1.17.15", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", - "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/jasmine": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.4.tgz", - "integrity": "sha512-px7OMFO/ncXxixDe1zR13V1iycqWae0MxTaw62RpFlksUi5QuNWgQJFkTQjIOvrmutJbI7Fp2Y2N1F6D2R4G6w==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "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==", + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/lodash": { - "version": "4.17.7", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", - "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/lodash-es": { - "version": "4.17.12", - "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", - "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/lodash": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/mute-stream": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", - "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/node": { - "version": "20.16.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", - "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/node-forge": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", - "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "optional": true, + "os": [ + "openbsd" + ] }, - "node_modules/@types/qs": { - "version": "6.9.15", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@types/retry": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", - "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@types/serve-index": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", - "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/express": "*" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "node_modules/@schematics/angular": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.1.3.tgz", + "integrity": "sha512-obJvWBhzRdsYL2msM4+8bQD21vFl3VxaVsuiq6iIfYsxhU5i2Iar2wM9NaRaIIqAYhZ8ehQQ/moB9BEbWvDCTw==", "dev": true, "license": "MIT", "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" + "@angular-devkit/core": "21.1.3", + "@angular-devkit/schematics": "21.1.3", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@types/sockjs": { - "version": "0.3.36", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", - "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.3.tgz", + "integrity": "sha512-huEXd1tWQHwwN+0VGRT+vSVplV0KNrGFUGJzkIW6iJE1SQElxn6etMai+pSd5DJcePkx6+SuscVsxbfwf70hnA==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } } }, - "node_modules/@types/wrap-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", - "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/ws": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", - "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "node_modules/@schematics/angular/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "@types/node": "*" + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@vitejs/plugin-basic-ssl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", - "integrity": "sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==", + "node_modules/@schematics/angular/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { - "node": ">=14.6.0" + "node": ">= 20.19.0" }, - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "node_modules/@schematics/angular/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" } }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "node_modules/@sigstore/bundle": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "node_modules/@sigstore/core": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.1.0.tgz", + "integrity": "sha512-o5cw1QYhNQ9IroioJxpzexmPjfCe7gzafd2RY3qnMpxr4ZEja+Jad/U8sgFpaue6bOaF+z7RVkyKVV44FN+N8A==", "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "node_modules/@sigstore/protobuf-specs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.0.tgz", + "integrity": "sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==", "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "node_modules/@sigstore/sign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.0.tgz", + "integrity": "sha512-Vx1RmLxLGnSUqx/o5/VsCjkuN5L7y+vxEEwawvc7u+6WtX2W4GNa7b9HEjmcRWohw/d6BpATXmvOwc78m+Swdg==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@xtuc/ieee754": "^1.2.0" + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.3", + "proc-log": "^6.1.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "node_modules/@sigstore/tuf": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.1.tgz", + "integrity": "sha512-OPZBg8y5Vc9yZjmWCHrlWPMBqW5yd8+wFNl+thMdtcWz3vjVSoJQutF8YkrzI0SLGnkuFof4HSsWUhXrf219Lw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@xtuc/long": "4.2.2" + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "node_modules/@sigstore/verify": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.0.tgz", + "integrity": "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", - "dev": true, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "color": "^5.0.2", + "text-hex": "1.0.x" } }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" - } + "license": "MIT" }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@xtuc/long": "4.2.2" - } + "license": "MIT" }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", "dev": true, - "license": "BSD-3-Clause" + "license": "MIT" }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", "dev": true, - "license": "Apache-2.0" + "license": "MIT" }, - "node_modules/@yarnpkg/lockfile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true, - "license": "BSD-2-Clause" + "license": "MIT" }, - "node_modules/abbrev": { + "node_modules/@tufjs/canonical-json": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", "dev": true, - "license": "ISC", + "license": "MIT", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "node_modules/@tufjs/models": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", + "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", "dev": true, "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.1.1" }, "engines": { - "node": ">= 0.6" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" }, "engines": { - "node": ">=0.4.0" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^8" + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/adjust-sourcemap-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", - "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", - "dev": true, + "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==", "license": "MIT", "dependencies": { - "loader-utils": "^2.0.0", - "regex-parser": "^2.2.11" - }, - "engines": { - "node": ">=8.9" + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" } }, - "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, + "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==", "license": "MIT", "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" + "@babel/types": "^7.0.0" } }, - "node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dev": true, + "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==", "license": "MIT", "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" } }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, + "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==", "license": "MIT", "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" + "@babel/types": "^7.28.2" } }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "@types/node": "*" } }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } + "@types/connect": "*", + "@types/node": "*" } }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "@types/node": "*" } }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", "dev": true, "license": "MIT", "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "@types/node": "*" } }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, - "engines": [ - "node >= 0.8.0" - ], - "license": "Apache-2.0", - "bin": { - "ansi-html": "bin/ansi-html" - } + "license": "MIT" }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "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", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } + "license": "MIT" }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" } }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" } }, - "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==", + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "dev": true, - "license": "Python-2.0" + "license": "MIT" }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "node_modules/@types/jasmine": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-6.0.0.tgz", + "integrity": "sha512-18lgGsLmEh3VJk9eZ5wAjTISxdqzl6YOwu8UdMpolajN57QOCNbl+AbHUd+Yu9ItrsFdB+c8LSZSGNg8nHaguw==", "dev": true, "license": "MIT" }, - "node_modules/autoprefixer": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", - "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "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, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", "license": "MIT", "dependencies": { - "browserslist": "^4.23.3", - "caniuse-lite": "^1.0.30001646", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "@types/ms": "*", + "@types/node": "*" } }, - "node_modules/babel-loader": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", - "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "node_modules/@types/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", "dev": true, - "license": "MIT", - "dependencies": { - "find-cache-dir": "^4.0.0", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0", - "webpack": ">=5" - } + "license": "MIT" }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", - "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.2", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + "@types/lodash": "*" } }, - "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } + "license": "MIT" }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", - "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2", - "core-js-compat": "^3.38.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + "@types/node": "*" } }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", - "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", - "dev": true, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.1.tgz", + "integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==", "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + "undici-types": "~7.16.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==", + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "node_modules/@types/nodemailer": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.9.tgz", + "integrity": "sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow==", "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" + "license": "MIT", + "dependencies": { + "@types/node": "*" + } }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "node_modules/@types/pg": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==", "dev": true, "license": "MIT", - "engines": { - "node": "^4.5.0 || >= 5.9" + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" } }, - "node_modules/batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "dev": true, "license": "MIT" }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } + "license": "MIT" }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "@types/node": "*" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", "dev": true, "license": "MIT", "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" + "@types/http-errors": "*", + "@types/node": "*" } }, - "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" } }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", "dev": true, "license": "MIT", "dependencies": { - "ms": "2.0.0" + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" } }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, - "node_modules/bonjour-service": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", - "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", + "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.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.54.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, - "license": "ISC" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/@typescript-eslint/parser": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", + "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3" + }, + "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/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", + "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", "dev": true, "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.0", + "debug": "^4.4.3" }, "engines": { - "node": ">=8" + "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/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", "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": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" - }, - "bin": { - "browserslist": "cli.js" + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", + "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", "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": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "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/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", + "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", "dev": true, "license": "MIT", "dependencies": { - "run-applescript": "^7.0.0" + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "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/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "node_modules/@typescript-eslint/types": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/cacache": { - "version": "18.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", - "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", + "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "@npmcli/fs": "^3.1.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "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/cacache/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/@typescript-eslint/utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", + "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.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/cacache/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", + "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "@typescript-eslint/types": "8.54.0", + "eslint-visitor-keys": "^4.2.1" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "node_modules/@typescript-eslint/visitor-keys/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": "ISC" - }, - "node_modules/cacache/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" - }, + "license": "Apache-2.0", "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://opencollective.com/eslint" } }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz", + "integrity": "sha512-dOxxrhgyDIEUADhb/8OlV9JIqYLgos03YorAueTIeOUskLJSEsfwCByjbu98ctXitUN3znXKp0bYD/WHSudCeA==", "dev": true, "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, "engines": { - "node": ">= 0.4" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001651", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", - "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" + "vite": { + "optional": true } - ], - "license": "CC-BY-4.0" + } }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "tinyrainbow": "^3.0.3" }, - "engines": { - "node": ">=4" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", "dev": true, - "license": "MIT" - }, - "node_modules/chart.js": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz", - "integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==", "license": "MIT", "dependencies": { - "@kurkle/color": "^0.3.0" + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" }, - "engines": { - "pnpm": ">=8" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", "dev": true, "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "url": "https://opencollective.com/vitest" } }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6.0" + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause" + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", "engines": { - "node": ">=6" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "dependencies": { - "restore-cursor": "^5.0.0" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.6" } }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "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", - "engines": { - "node": ">=6" + "bin": { + "acorn": "bin/acorn" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=0.4.0" } }, - "node_modules/cli-truncate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "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/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "dev": true, "license": "MIT", "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" + "acorn": "^8.11.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.4.0" } }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, - "license": "ISC", + "license": "MIT", "engines": { - "node": ">= 12" + "node": ">= 14" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, - "engines": { - "node": ">=12" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/cliui/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==", + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "ajv": "^8.0.0" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "ajv": "^8.0.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "node_modules/cliui/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==", + "node_modules/algoliasearch": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.46.2.tgz", + "integrity": "sha512-qqAXW9QvKf2tTyhpDA4qXv1IfBwD2eduSW6tUEBFIfCeE9gn9HQ9I5+MaKoenRuHrzk5sQoNh1/iof8mY7uD6Q==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "@algolia/abtesting": "1.12.2", + "@algolia/client-abtesting": "5.46.2", + "@algolia/client-analytics": "5.46.2", + "@algolia/client-common": "5.46.2", + "@algolia/client-insights": "5.46.2", + "@algolia/client-personalization": "5.46.2", + "@algolia/client-query-suggestions": "5.46.2", + "@algolia/client-search": "5.46.2", + "@algolia/ingestion": "1.46.2", + "@algolia/monitoring": "1.46.2", + "@algolia/recommend": "5.46.2", + "@algolia/requester-browser-xhr": "5.46.2", + "@algolia/requester-fetch": "5.46.2", + "@algolia/requester-node-http": "5.46.2" }, "engines": { - "node": ">=7.0.0" + "node": ">= 14.0.0" } }, - "node_modules/cliui/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/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/angular-eslint": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/angular-eslint/-/angular-eslint-21.2.0.tgz", + "integrity": "sha512-pERqqHIMwD34UT0FoHSNTt4V332vHiAzgkY0rgdUaqSamS94IzbF02EfFxygr53UogQQOXhpLbSSDMOyovB3TA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "@angular-devkit/core": ">= 21.0.0 < 22.0.0", + "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", + "@angular-eslint/builder": "21.2.0", + "@angular-eslint/eslint-plugin": "21.2.0", + "@angular-eslint/eslint-plugin-template": "21.2.0", + "@angular-eslint/schematics": "21.2.0", + "@angular-eslint/template-parser": "21.2.0", + "@typescript-eslint/types": "^8.0.0", + "@typescript-eslint/utils": "^8.0.0" + }, + "peerDependencies": { + "@angular/cli": ">= 21.0.0 < 22.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*", + "typescript-eslint": "^8.0.0" } }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/angular-eslint/node_modules/@angular-devkit/core": { + "version": "21.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.3.tgz", + "integrity": "sha512-huEXd1tWQHwwN+0VGRT+vSVplV0KNrGFUGJzkIW6iJE1SQElxn6etMai+pSd5DJcePkx6+SuscVsxbfwf70hnA==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" }, "engines": { - "node": ">=8" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } } }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/angular-eslint/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "readdirp": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">= 20.19.0" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://paulmillr.com/funding/" } }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "node_modules/angular-eslint/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { - "node": ">=0.8" + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "node_modules/angular-eslint/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, + "license": "BSD-3-Clause", "engines": { - "node": ">=6" + "node": ">= 12" } }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, + "node_modules/angularx-flatpickr": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/angularx-flatpickr/-/angularx-flatpickr-8.1.0.tgz", + "integrity": "sha512-U+WXMUXGEiQbdMGSPk7T+HehmAFdVWKu3XlCXFM8mYCCB/fWHW8sbHstxxZgOymD5Q1kfLaHNob1MxhWUgv1hg==", "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=17.0.0", + "@angular/forms": ">=17.0.0", + "flatpickr": "^4.5.0" } }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/common-path-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", - "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", - "dev": true, - "license": "ISC" - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": ">= 1.43.0 < 2" + "environment": "^1.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, "engines": { - "node": ">= 0.8.0" + "node": ">=8" } }, - "node_modules/compression/node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "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": ">= 0.8" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "ms": "2.0.0" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "node_modules/anymatch/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" + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "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==", + "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": "MIT" + "license": "Python-2.0" }, - "node_modules/connect": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", - "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "finalhandler": "1.1.2", - "parseurl": "~1.3.3", - "utils-merge": "1.0.1" - }, + "license": "Apache-2.0", "engines": { - "node": ">= 0.10.0" + "node": ">= 0.4" } }, - "node_modules/connect-history-api-fallback": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", - "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } + "license": "MIT" }, - "node_modules/connect/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">=12" } }, - "node_modules/connect/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, + "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.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", "license": "MIT", "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" } }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">= 0.6" + "node": ">= 0.4" } }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "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/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": "^4.5.0 || >= 5.9" } }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true, - "license": "MIT" + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } }, - "node_modules/copy-anything": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", - "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", - "dev": true, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", "license": "MIT", "dependencies": { - "is-what": "^3.14.1" + "safe-buffer": "5.1.2" }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" + "engines": { + "node": ">= 0.8" } }, - "node_modules/copy-webpack-plugin": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", - "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/beasties": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.3.5.tgz", + "integrity": "sha512-NaWu+f4YrJxEttJSm16AzMIFtVldCvaJ68b1L098KpqXmxt9xOLtKoLkKxb8ekhOrLqEJAbvT6n6SEvB/sac7A==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.1", - "globby": "^14.0.0", - "normalize-path": "^3.0.0", - "schema-utils": "^4.2.0", - "serialize-javascript": "^6.0.2" + "css-select": "^6.0.0", + "css-what": "^7.0.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^10.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3" }, "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" + "node": ">=14.0.0" } }, - "node_modules/copy-webpack-plugin/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==", + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, + "license": "MIT", "engines": { - "node": ">=10.13.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/core-js-compat": { - "version": "3.38.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", - "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==", - "dev": true, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { - "browserslist": "^4.23.3" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/core-js" + "url": "https://opencollective.com/express" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "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==", + "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": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" + "balanced-match": "^1.0.0" } }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "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": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "fill-range": "^7.1.1" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=8" } }, - "node_modules/critters": { - "version": "0.0.24", - "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.24.tgz", - "integrity": "sha512-Oyqew0FGM0wYUSNqR0L6AteO5MpMoUU0rhKRieXeiKs+PmRTxiJMyaunYB2KF6fQ3dzChXKCpbFOEJx3OQ1v/Q==", - "dev": true, - "license": "Apache-2.0", + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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": { - "chalk": "^4.1.0", - "css-select": "^5.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.2", - "htmlparser2": "^8.0.2", - "postcss": "^8.4.23", - "postcss-media-query-parser": "^0.2.3" + "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/critters/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==", + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">= 0.8" } }, - "node_modules/critters/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/cacache": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", + "integrity": "sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0", + "unique-filename": "^5.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/critters/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==", + "node_modules/cacache/node_modules/glob": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", + "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "color-name": "~1.1.4" + "minimatch": "^10.1.2", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" }, "engines": { - "node": ">=7.0.0" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/critters/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/critters/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==", + "node_modules/cacache/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=8" + "node": "20 || >=22" } }, - "node_modules/critters/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==", + "node_modules/cacache/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "has-flag": "^4.0.0" + "@isaacs/brace-expansion": "^5.0.1" }, "engines": { - "node": ">=8" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, + "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": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { - "node": ">= 8" + "node": ">= 0.4" } }, - "node_modules/cross-spawn/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", + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { - "node": ">= 8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/css-loader": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", - "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "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", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.27.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" }, - "webpack": { - "optional": true + { + "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/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "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": "BSD-2-Clause", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">= 6" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/fb55" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "node_modules/chalk/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", - "bin": { - "cssesc": "bin/cssesc" + "dependencies": { + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/custom-event": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", - "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", "dev": true, "license": "MIT" }, - "node_modules/date-format": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", - "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", - "dev": true, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, "engines": { - "node": ">=4.0" + "pnpm": ">=8" } }, - "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { - "ms": "2.1.2" + "readdirp": "^4.0.1" }, "engines": { - "node": ">=6.0" + "node": ">= 14.16.0" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/default-browser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, - "license": "MIT", - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, + "license": "BlueOak-1.0.0", "engines": { "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/default-browser-id": { + "node_modules/cli-cursor": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, "engines": { "node": ">=18" }, @@ -6532,1769 +6785,1751 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", "dev": true, "license": "MIT", - "dependencies": { - "clone": "^1.0.2" + "engines": { + "node": ">=18.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "node_modules/cli-truncate": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", "dev": true, "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=20" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz", + "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==", "dev": true, "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, "engines": { - "node": ">= 0.8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, - "license": "Apache-2.0", + "license": "ISC", "engines": { - "node": ">=8" + "node": ">= 12" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT" - }, - "node_modules/di": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", - "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", - "dev": true, - "license": "MIT" - }, - "node_modules/dns-packet": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", - "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">=6" + "node": ">=12" } }, - "node_modules/dom-serialize": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", - "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", - "dev": true, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", "license": "MIT", "dependencies": { - "custom-event": "~1.0.0", - "ent": "~2.2.0", - "extend": "^3.0.0", - "void-elements": "^2.0.0" + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" } }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "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": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" + "color-name": "~1.1.4" }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + "engines": { + "node": ">=7.0.0" } }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "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, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" + "license": "MIT" }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", "dependencies": { - "domelementtype": "^2.3.0" + "color-name": "^2.0.0" }, "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" + "node": ">=18" } }, - "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" + "color-name": "^2.0.0" }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" + "engines": { + "node": ">=14.6" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.13", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", - "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", - "dev": true, - "license": "MIT" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, + "node_modules/color/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", "license": "MIT", "engines": { - "node": ">= 4" + "node": ">=12.20" } }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "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/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "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/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" }, "engines": { - "node": ">=0.10.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, - "node_modules/engine.io": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", - "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/cookie": "^0.4.1", - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.4.1", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" }, "engines": { - "node": ">=10.2.0" + "node": ">= 0.10.0" } }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10.0.0" + "dependencies": { + "ms": "2.0.0" } }, - "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", - "dev": true, + "node_modules/connect/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, "engines": { - "node": ">=10.13.0" + "node": ">= 0.8" } }, - "node_modules/ent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz", - "integrity": "sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A==", + "node_modules/connect/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", "dev": true, "license": "MIT", "dependencies": { - "punycode": "^1.4.1" + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.8" } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "devOptional": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/connect/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "engines": { + "node": ">= 0.8" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "node_modules/connect/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 0.6" } }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "license": "MIT", "engines": { "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "dev": true, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", - "optional": true, - "dependencies": { - "prr": "~1.0.1" - }, - "bin": { - "errno": "cli.js" + "engines": { + "node": ">= 0.6" } }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, "engines": { - "node": ">= 0.4" + "node": ">= 0.6" } }, - "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==", - "dev": true, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=6.6.0" } }, - "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true, "license": "MIT" }, - "node_modules/esbuild": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", - "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", - "dev": true, - "hasInstallScript": true, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "dependencies": { + "object-assign": "^4", + "vary": "^1" }, "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.0", - "@esbuild/android-arm": "0.23.0", - "@esbuild/android-arm64": "0.23.0", - "@esbuild/android-x64": "0.23.0", - "@esbuild/darwin-arm64": "0.23.0", - "@esbuild/darwin-x64": "0.23.0", - "@esbuild/freebsd-arm64": "0.23.0", - "@esbuild/freebsd-x64": "0.23.0", - "@esbuild/linux-arm": "0.23.0", - "@esbuild/linux-arm64": "0.23.0", - "@esbuild/linux-ia32": "0.23.0", - "@esbuild/linux-loong64": "0.23.0", - "@esbuild/linux-mips64el": "0.23.0", - "@esbuild/linux-ppc64": "0.23.0", - "@esbuild/linux-riscv64": "0.23.0", - "@esbuild/linux-s390x": "0.23.0", - "@esbuild/linux-x64": "0.23.0", - "@esbuild/netbsd-x64": "0.23.0", - "@esbuild/openbsd-arm64": "0.23.0", - "@esbuild/openbsd-x64": "0.23.0", - "@esbuild/sunos-x64": "0.23.0", - "@esbuild/win32-arm64": "0.23.0", - "@esbuild/win32-ia32": "0.23.0", - "@esbuild/win32-x64": "0.23.0" - } - }, - "node_modules/esbuild-wasm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.23.0.tgz", - "integrity": "sha512-6jP8UmWy6R6TUUV8bMuC3ZyZ6lZKI56x0tkxyCIqWwRRJ/DgeQKneh/Oid5EoGoPFLrGNkz47ZEtWAYuiY/u9g==", - "dev": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "node": ">= 0.10" }, - "engines": { - "node": ">=18" - } - }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true, "license": "MIT" }, - "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==", + "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", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=8.0.0" + "node": ">= 8" } }, - "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==", + "node_modules/css-select": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "estraverse": "^5.2.0" + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" }, - "engines": { - "node": ">=4.0" + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "node_modules/css-what": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=4.0" + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } + "license": "MIT" }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=4.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, "engines": { - "node": ">= 0.6" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "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/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, + "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.8.x" + "node": ">=0.4.0" } }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "node": ">= 0.8" } }, - "node_modules/execa/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true, "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/exponential-backoff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", - "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } }, - "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" + "asap": "^2.0.0", + "wrappy": "1" } }, - "node_modules/express/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", "dev": true, - "license": "MIT", + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">= 0.6" + "node": ">=0.3.1" } }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", "dev": true, "license": "MIT", "dependencies": { - "ms": "2.0.0" + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" } }, - "node_modules/express/node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" }, - "engines": { - "node": ">= 0.8" + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, - "license": "MIT" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" }, - "node_modules/express/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, "engines": { - "node": ">= 0.8" + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true, - "license": "MIT" - }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" }, - "engines": { - "node": ">=4" + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "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/dotenv": { + "version": "17.2.4", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.4.tgz", + "integrity": "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, + "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": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, "engines": { - "node": ">=8.6.0" + "node": ">= 0.4" } }, - "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, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, - "node_modules/fast-uri": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", - "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" + "iconv-lite": "^0.6.2" } }, - "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==", + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "to-regex-range": "^5.0.1" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "node_modules/engine.io": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", "dev": true, "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" }, "engines": { - "node": ">= 0.8" + "node": ">=10.2.0" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "dev": true, "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">=10.0.0" } }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/finalhandler/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "dev": true, "license": "MIT", "dependencies": { - "ee-first": "1.1.1" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, - "node_modules/find-cache-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", - "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "node_modules/engine.io/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==", "dev": true, "license": "MIT", - "dependencies": { - "common-path-prefix": "^3.0.0", - "pkg-dir": "^7.0.0" - }, "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.6" } }, - "node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "node_modules/engine.io/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==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" + "mime-db": "1.52.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.6" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true, - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], "license": "MIT", "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "node": ">= 0.6" } }, - "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "node_modules/ent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", + "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "punycode": "^1.4.1", + "safe-regex-test": "^1.1.0" }, "engines": { - "node": ">=14" + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=6" } }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "dev": true, "license": "MIT", "engines": { - "node": "*" + "node": ">=18" }, "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "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.6" + "node": ">= 0.4" } }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, + "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", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, "engines": { - "node": ">=6 <7 || >=8" + "node": ">= 0.4" } }, - "node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, - "license": "ISC", + "license": "MIT" + }, + "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": { - "minipass": "^7.0.3" + "es-errors": "^1.3.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">= 0.4" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "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, + "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", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">= 0.4" } }, - "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==", + "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", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "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/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, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": ">=6" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" }, - "node_modules/get-east-asian-width": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", - "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "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": ">=18" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "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": ">= 0.4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "node_modules/eslint-scope": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.0.tgz", + "integrity": "sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, "engines": { - "node": ">=10" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "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": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, + "license": "Apache-2.0", "engines": { - "node": "*" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://opencollective.com/eslint" } }, - "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==", + "node_modules/eslint/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": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, - "engines": { - "node": ">= 6" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "node_modules/eslint/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", - "engines": { - "node": ">=4" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/globby": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", - "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "node_modules/eslint/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": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "@sindresorhus/merge-streams": "^2.1.0", - "fast-glob": "^3.3.2", - "ignore": "^5.2.4", - "path-type": "^5.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.1.0" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "node_modules/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": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/eslint" } }, - "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==", + "node_modules/eslint/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": "ISC" + "license": "MIT", + "engines": { + "node": ">= 4" + } }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "node_modules/eslint/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/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/eslint/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": "MIT", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { - "node": ">=4" + "node": "*" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "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": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "es-define-property": "^1.0.0" + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, - "license": "MIT", "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/eslint" } }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "node_modules/espree/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": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/eslint" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "function-bind": "^1.1.2" + "estraverse": "^5.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10" } }, - "node_modules/hosted-git-info": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "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": "ISC", + "license": "BSD-2-Clause", "dependencies": { - "lru-cache": "^10.0.1" + "estraverse": "^5.2.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">=4.0" } }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "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": "ISC" + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" + "@types/estree": "^1.0.0" } }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "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/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "engines": { + "node": ">= 0.6" } }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "dev": true, "license": "MIT" }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", "dev": true, "license": "MIT", "dependencies": { - "safe-buffer": "~5.1.0" + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/html-entities": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", - "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } }, - "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], "license": "MIT", "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" } }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true, - "license": "BSD-2-Clause" + "license": "MIT" }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "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/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "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", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } + "license": "MIT" }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "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/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/http-parser-js": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", - "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", - "dev": true, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "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": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" + "flat-cache": "^4.0.0" }, "engines": { - "node": ">=8.0.0" + "node": ">=16.0.0" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "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": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">= 14" + "node": ">=8" } }, - "node_modules/http-proxy-middleware": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.0.tgz", - "integrity": "sha512-36AV1fIaI2cWRzHo+rbcxhe3M3jUDCNzc4D5zRl57sEWRAxdXYtw7FSQKYY6PDKssiAKjLYypbssHk+xs/kMXw==", - "dev": true, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { - "@types/http-proxy": "^1.17.10", - "debug": "^4.3.4", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.5" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, - "node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "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": { - "agent-base": "^7.0.2", - "debug": "4" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">= 14" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/hyperdyperid": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", - "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.18" - } + "node_modules/flag-icons": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.5.0.tgz", + "integrity": "sha512-kd+MNXviFIg5hijH766tt+3x76ele1AXlo4zDdCxIvqWZhKt4T83bOtxUOOMlTx/EcFdUMH5yvQgYlFh1EqqFg==", + "license": "MIT" }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "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": { - "safer-buffer": ">= 2.1.2 < 3" + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, "engines": { - "node": ">=0.10.0" + "node": ">=16" } }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } + "node_modules/flatpickr": { + "version": "4.6.13", + "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz", + "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==", + "license": "MIT" }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "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": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" } ], - "license": "BSD-3-Clause" - }, - "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": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } } }, - "node_modules/ignore-walk": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", - "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", - "dev": true, - "license": "ISC", + "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": { - "minimatch": "^9.0.0" + "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": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">= 6" } }, - "node_modules/ignore-walk/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, + "node_modules/form-data/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", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/ignore-walk/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": ">= 0.6" } }, - "node_modules/image-size": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", - "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", - "dev": true, + "node_modules/form-data/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", - "optional": true, - "bin": { - "image-size": "bin/image-size.js" + "dependencies": { + "mime-db": "1.52.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.6" } }, - "node_modules/immutable": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", - "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", "dev": true, "license": "MIT", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" }, "engines": { - "node": ">=6" + "node": ">=14.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://ko-fi.com/tunnckoCore/commissions" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "license": "MIT", "engines": { - "node": ">=0.8.19" + "node": ">= 0.6" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, "engines": { - "node": ">=8" + "node": ">=6 <7 || >=8" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "dev": true, "license": "ISC", "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, "license": "ISC" }, - "node_modules/ini": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", - "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, - "license": "ISC", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "dev": true, + "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", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", - "dev": true, + "node_modules/g": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/g/-/g-2.0.1.tgz", + "integrity": "sha512-Fi6Ng5fZ/ANLQ15H11hCe+09sgUoNvDEBevVgx3KoYOhsH5iLNPn54hx0jPZ+3oSWr+xajnp2Qau9VmPsc7hTA==", + "license": "MIT" + }, + "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==", "license": "MIT", "engines": { - "node": ">= 10" + "node": ">=6.9.0" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "dev": true, + "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": { - "hasown": "^2.0.2" + "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" @@ -8303,496 +8538,830 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, + "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", - "bin": { - "is-docker": "cli.js" + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.4" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/get-tsconfig": { + "version": "4.13.5", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.5.tgz", + "integrity": "sha512-v4/4xAEpBRp6SvCkWhnGCaLkJf9IwWzrsygJPxD/+p2/xPE3C5m2fA9FD0Ry9tG+Rqqq3gBzHSl6y1/T9V/tMQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, - "license": "MIT", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, "engines": { - "node": ">=12" + "node": "*" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, - "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==", + "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": "MIT", + "license": "ISC", "dependencies": { - "is-extglob": "^2.1.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">=0.10.0" + "node": ">=10.13.0" } }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/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": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/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": ">=14.16" + "node": "*" + } + }, + "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/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, + "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": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "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": "MIT" + "license": "ISC" }, - "node_modules/is-network-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", - "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "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": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "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, + "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.12.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true, + "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": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, + "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": { - "isobject": "^3.0.1" + "function-bind": "^1.1.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/hono": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.8.tgz", + "integrity": "sha512-eVkB/CYCCei7K2WElZW9yYQFWssG0DhaDhVvr7wy5jJ22K+ck8fWW0EsLpB0sITUTvPnc97+rrbQqIr5iqiy9Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=16.9.0" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", + "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "20 || >=22" } }, - "node_modules/is-what": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", - "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, "license": "MIT" }, - "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], "license": "MIT", "dependencies": { - "is-inside-container": "^1.0.0" - }, + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", "engines": { - "node": ">=16" + "node": ">=0.12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "dev": true, - "license": "MIT" + "license": "BSD-2-Clause" }, - "node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", - "dev": true, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, "engines": { - "node": ">= 8.0.0" + "node": ">= 0.8" }, "funding": { - "url": "https://github.com/sponsors/gjtorikian/" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "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/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8.0.0" } }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, "engines": { - "node": ">=8" + "node": ">= 14" } }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" + "agent-base": "^7.1.2", + "debug": "4" }, "engines": { - "node": ">=10" + "node": ">= 14" } }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">=10" + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/istanbul-lib-report/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==", + "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": ">=8" + "node": ">= 4" } }, - "node_modules/istanbul-lib-report/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==", + "node_modules/ignore-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "has-flag": "^4.0.0" + "minimatch": "^10.0.3" }, "engines": { - "node": ">=8" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", "dev": true, - "license": "BSD-3-Clause", + "license": "BlueOak-1.0.0", "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" + "@isaacs/brace-expansion": "^5.0.1" }, "engines": { - "node": ">=10" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/istanbul-lib-source-maps/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, - "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "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": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "once": "^1.3.0", + "wrappy": "1" } }, - "node_modules/jasmine-core": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.2.0.tgz", - "integrity": "sha512-tSAtdrvWybZkQmmaIoDgnvHG8ORUNw5kEVlO5CvrXj02Jjr9TZrmjFq7FUiOUzJiOP2wLGYT6PgrQgQF4R1xiw==", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", "dev": true, - "license": "MIT" + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, "engines": { - "node": ">= 10.13.0" + "node": ">= 12" } }, - "node_modules/jest-worker/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==", + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "hasown": "^2.0.2" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jiti": { - "version": "1.21.6", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", - "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "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", - "bin": { - "jiti": "bin/jiti.js" + "engines": { + "node": ">=0.10.0" } }, - "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==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "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": { - "argparse": "^2.0.1" + "is-extglob": "^2.1.1" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", "dev": true, "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, "engines": { - "node": ">=4" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/json-parse-even-better-errors": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", - "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "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": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=0.12.0" } }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "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==", + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "license": "MIT", - "bin": { - "json5": "lib/cli.js" + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { - "node": ">=6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true, - "license": "MIT" + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", "dev": true, "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jsonparse": { - "version": "1.3.1", + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "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/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/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/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jasmine-core": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-6.0.1.tgz", + "integrity": "sha512-gUtzV5ASR0MLBwDNqri4kBsgKNCcRQd9qOlNw/w/deavD0cl3JmWXXfH8JhKM4LTg6LPTt2IOQ4px3YYfgh2Xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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==", + "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==", + "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-parse-even-better-errors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "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==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", "dev": true, @@ -8801,6 +9370,49 @@ ], "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/karma": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", @@ -8850,6 +9462,19 @@ "which": "^1.2.1" } }, + "node_modules/karma-chrome-launcher/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/karma-coverage": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", @@ -8868,6 +9493,17 @@ "node": ">=10.0.0" } }, + "node_modules/karma-coverage/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/karma-coverage/node_modules/istanbul-lib-instrument": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", @@ -8885,6 +9521,19 @@ "node": ">=8" } }, + "node_modules/karma-coverage/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/karma-coverage/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -8912,13 +9561,13 @@ } }, "node_modules/karma-jasmine-html-reporter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz", - "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.2.0.tgz", + "integrity": "sha512-J0laEC43Oy2RdR5V5R3bqmdo7yRIYySq6XHKbA+e5iSAgLjhR1oICLGeSREPlJXpeyNcdJf3J17YcdhD0mRssQ==", "dev": true, "license": "MIT", "peerDependencies": { - "jasmine-core": "^4.0.0 || ^5.0.0", + "jasmine-core": "^4.0.0 || ^5.0.0 || ^6.0.0", "karma": "^6.0.0", "karma-jasmine": "^5.0.0" } @@ -8930,30 +9579,65 @@ "dev": true, "license": "MIT" }, - "node_modules/karma-source-map-support": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", - "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "node_modules/karma/node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "dev": true, "license": "MIT", "dependencies": { - "source-map-support": "^0.5.5" + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/karma/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==", + "node_modules/karma/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": { - "color-convert": "^2.0.1" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" }, "engines": { - "node": ">=8" + "node": ">= 8.10.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, "node_modules/karma/node_modules/cliui": { @@ -8968,284 +9652,235 @@ "wrap-ansi": "^7.0.0" } }, - "node_modules/karma/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==", + "node_modules/karma/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "ms": "2.0.0" } }, - "node_modules/karma/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/karma/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/karma/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/karma/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": "MIT", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, "engines": { - "node": ">=8" + "node": ">= 6" } }, - "node_modules/karma/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/karma/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/karma/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/karma/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true, "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, "engines": { - "node": ">=8" + "node": ">= 0.6" } }, - "node_modules/karma/node_modules/tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "node_modules/karma/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==", "dev": true, "license": "MIT", "engines": { - "node": ">=14.14" + "node": ">= 0.6" } }, - "node_modules/karma/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/karma/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==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "mime-db": "1.52.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">= 0.6" } }, - "node_modules/karma/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "node_modules/karma/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": "MIT", + "license": "ISC", "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=10" + "node": "*" } }, - "node_modules/karma/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "node_modules/karma/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } + "license": "MIT" }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/karma/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": ">=0.10.0" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/launch-editor": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.8.1.tgz", - "integrity": "sha512-elBx2l/tp9z99X5H/qev8uyDywVh0VXAwEbjk8kJhnc5grOFkGh7aW6q55me9xnYbss261XtnUrysZ+XvGbhQA==", + "node_modules/karma/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "dev": true, "license": "MIT", "dependencies": { - "picocolors": "^1.0.0", - "shell-quote": "^1.8.1" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "node_modules/less": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", - "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "node_modules/karma/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "copy-anything": "^2.0.1", - "parse-node-version": "^1.0.1", - "tslib": "^2.3.0" - }, - "bin": { - "lessc": "bin/lessc" + "picomatch": "^2.2.1" }, "engines": { - "node": ">=6" - }, - "optionalDependencies": { - "errno": "^0.1.1", - "graceful-fs": "^4.1.2", - "image-size": "~0.5.0", - "make-dir": "^2.1.0", - "mime": "^1.4.1", - "needle": "^3.1.0", - "source-map": "~0.6.0" + "node": ">=8.10.0" } }, - "node_modules/less-loader": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", - "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", + "node_modules/karma/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "less": "^3.5.0 || ^4.0.0", - "webpack": "^5.0.0" + "bin": { + "rimraf": "bin.js" }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/less/node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "node_modules/karma/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" + "media-typer": "0.3.0", + "mime-types": "~2.1.24" }, "engines": { - "node": ">=6" + "node": ">= 0.6" } }, - "node_modules/less/node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "license": "MIT", - "optional": true, - "bin": { - "mime": "cli.js" + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" }, "engines": { - "node": ">=4" + "node": ">=10" } }, - "node_modules/less/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver" + "engines": { + "node": ">=10" } }, - "node_modules/less/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "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": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" } }, - "node_modules/license-webpack-plugin": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", - "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "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": "ISC", + "license": "MIT", "dependencies": { - "webpack-sources": "^3.0.0" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-sources": { - "optional": true - } + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, "node_modules/listr2": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", - "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, "license": "MIT", "dependencies": { - "cli-truncate": "^4.0.0", + "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", @@ -9253,13 +9888,13 @@ "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -9270,9 +9905,9 @@ } }, "node_modules/listr2/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { @@ -9282,17 +9917,42 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, "node_modules/listr2/node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "dev": true, "license": "MIT" }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/listr2/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, "license": "MIT", "dependencies": { @@ -9306,9 +9966,9 @@ } }, "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { @@ -9324,178 +9984,134 @@ } }, "node_modules/lmdb": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.0.13.tgz", - "integrity": "sha512-UGe+BbaSUQtAMZobTb4nHvFMrmvuAQKSeaqAX2meTEQjfsbpl5sxdHD8T72OnwD4GU9uwNhYXIVe4QGs8N9Zyw==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.4.4.tgz", + "integrity": "sha512-+Y2DqovevLkb6DrSQ6SXTYLEd6kvlRbhsxzgJrk7BUfOVA/mt21ak6pFDZDKxiAczHMWxrb02kXBTSTIA0O94A==", "dev": true, "hasInstallScript": true, "license": "MIT", + "optional": true, "dependencies": { - "msgpackr": "^1.10.2", + "msgpackr": "^1.11.2", "node-addon-api": "^6.1.0", "node-gyp-build-optional-packages": "5.2.2", - "ordered-binary": "^1.4.1", + "ordered-binary": "^1.5.3", "weak-lru-cache": "^1.2.2" }, "bin": { "download-lmdb-prebuilds": "bin/download-prebuilds.js" }, "optionalDependencies": { - "@lmdb/lmdb-darwin-arm64": "3.0.13", - "@lmdb/lmdb-darwin-x64": "3.0.13", - "@lmdb/lmdb-linux-arm": "3.0.13", - "@lmdb/lmdb-linux-arm64": "3.0.13", - "@lmdb/lmdb-linux-x64": "3.0.13", - "@lmdb/lmdb-win32-x64": "3.0.13" - } - }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.11.5" + "@lmdb/lmdb-darwin-arm64": "3.4.4", + "@lmdb/lmdb-darwin-x64": "3.4.4", + "@lmdb/lmdb-linux-arm": "3.4.4", + "@lmdb/lmdb-linux-arm64": "3.4.4", + "@lmdb/lmdb-linux-x64": "3.4.4", + "@lmdb/lmdb-win32-arm64": "3.4.4", + "@lmdb/lmdb-win32-x64": "3.4.4" } }, - "node_modules/loader-utils": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", - "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "node_modules/lmdb/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } + "optional": true }, "node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "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": "^6.0.0" + "p-locate": "^5.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "license": "MIT" }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" }, - "node_modules/log-symbols/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/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" }, - "node_modules/log-symbols/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/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" }, - "node_modules/log-symbols/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/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" }, - "node_modules/log-symbols/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, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, - "node_modules/log-symbols/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==", + "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", - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/log-symbols/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==", + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/log-update": { @@ -9518,26 +10134,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -9548,9 +10148,9 @@ } }, "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { @@ -9560,43 +10160,35 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", - "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", - "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/log-update/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, "license": "MIT", "dependencies": { @@ -9610,9 +10202,9 @@ } }, "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { @@ -9644,24 +10236,49 @@ "node": ">=8.0" } }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "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/magic-string": { - "version": "0.30.11", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", - "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/make-dir": { @@ -9680,82 +10297,64 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/make-fetch-happen": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", - "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz", + "integrity": "sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/agent": "^2.0.0", - "cacache": "^18.0.0", + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", - "is-lambda": "^1.0.1", "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", + "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "proc-log": "^4.2.0", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", "promise-retry": "^2.0.1", - "ssri": "^10.0.0" + "ssri": "^13.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, + "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.6" + "node": ">= 0.4" } }, - "node_modules/memfs": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.11.1.tgz", - "integrity": "sha512-LZcMTBAgqUUKNXZagcZxvXXfgF1bHX7Y7nQ0QyEiNbRJgE29GhgPd8Yna1VQcLlPiHt/5RFJMWYN9Uv/VPNvjQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/json-pack": "^1.0.3", - "@jsonjoy.com/util": "^1.3.0", - "tree-dump": "^1.0.1", - "tslib": "^2.0.0" - }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", "engines": { - "node": ">= 4.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "node": ">= 0.8" } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "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, + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/methods": { @@ -9768,33 +10367,6 @@ "node": ">= 0.6" } }, - "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": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -9809,36 +10381,28 @@ } }, "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==", - "dev": true, + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "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==", - "dev": true, + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/mimic-function": { @@ -9854,45 +10418,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mini-css-extract-plugin": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", - "integrity": "sha512-Zs1YsZVfemekSZG+44vBsYTLQORkPMwnlv+aehcxK/NLKC+EGhDB39/YePYYqx/sTk6NnYpuqikhSn7+JIevTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "schema-utils": "^4.0.0", - "tapable": "^2.2.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true, - "license": "ISC" - }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -9929,18 +10468,18 @@ } }, "node_modules/minipass-fetch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", - "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.1.tgz", + "integrity": "sha512-yHK8pb0iCGat0lDrs/D6RZmCdaBT64tULXjdxjSMAqoDi18Q3qKEUTHypHQZQd9+FYpIS+lkvpq6C/R6SbUeRw==", "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" }, "optionalDependencies": { "encoding": "^0.1.13" @@ -10013,89 +10552,91 @@ "license": "ISC" }, "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", "dev": true, "license": "ISC", "dependencies": { - "minipass": "^3.0.0" + "minipass": "^7.1.2" }, "engines": { "node": ">=8" } }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" + "minipass": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">= 18" } }, - "node_modules/minipass-sized/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "minimist": "^1.2.6" }, - "engines": { - "node": ">= 8" + "bin": { + "mkdirp": "bin/cmd.js" } }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" }, "engines": { - "node": ">=8" + "node": ">= 0.8.0" } }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", "license": "MIT", "dependencies": { - "minimist": "^1.2.6" + "ee-first": "1.1.1" }, - "bin": { - "mkdirp": "bin/cmd.js" + "engines": { + "node": ">= 0.8" } }, "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "dev": true, "license": "MIT", "engines": { @@ -10103,18 +10644,18 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/msgpackr": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.0.tgz", - "integrity": "sha512-I8qXuuALqJe5laEBYoFykChhSXLikZmUhccjGsPuSJ/7uPip2TJ7lwdIQwWSAi0jGZDXv4WOP8Qg65QZRuXxXw==", + "version": "1.11.8", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.8.tgz", + "integrity": "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA==", "dev": true, "license": "MIT", + "optional": true, "optionalDependencies": { "msgpackr-extract": "^3.0.2" } @@ -10142,34 +10683,20 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" } }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "dev": true, - "license": "MIT", - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, "node_modules/mute-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", "dev": true, "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -10185,147 +10712,77 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/needle": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", - "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.3", - "sax": "^1.2.4" - }, - "bin": { - "needle": "bin/needle" - }, - "engines": { - "node": ">= 4.4.x" - } - }, - "node_modules/needle/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "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", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, "node_modules/ngx-scrollbar": { - "version": "13.0.3", - "resolved": "https://registry.npmjs.org/ngx-scrollbar/-/ngx-scrollbar-13.0.3.tgz", - "integrity": "sha512-ZpjNm9y7F5OSQh7p4x2PrwRkNl5zARPqPbRdkS5HjP8lHpG7hAFpbsjW+WlsM/6lAL5pesfVRCSlvcRJihM2sg==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/ngx-scrollbar/-/ngx-scrollbar-19.1.4.tgz", + "integrity": "sha512-VzmLli1Uq+tZ40s5BpBDVO5mxjeNL7L7JbVluuokKsrIfi1GmXmkx69iz0zruyVWmyzU5VeuiqhMobPYazGKOQ==", "license": "MIT", "dependencies": { - "tslib": "^2.3.1" + "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/cdk": ">=16.0.0", - "@angular/common": ">=16.0.0", - "@angular/core": ">=16.0.0", + "@angular/cdk": ">=20.0.0", + "@angular/common": ">=20.0.0", + "@angular/core": ">=20.0.0", "rxjs": ">=7.0.0" } }, - "node_modules/nice-napi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", - "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "!win32" - ], - "dependencies": { - "node-addon-api": "^3.0.0", - "node-gyp-build": "^4.2.2" - } - }, - "node_modules/nice-napi/node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "dev": true, "license": "MIT", "optional": true }, - "node_modules/node-addon-api": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true, - "license": "(BSD-3-Clause OR GPL-2.0)", + "node_modules/node-cron": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", "engines": { - "node": ">= 6.13.0" + "node": ">=6.0.0" } }, "node_modules/node-gyp": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.2.0.tgz", - "integrity": "sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==", + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", + "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", "dev": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", - "glob": "^10.3.10", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^13.0.0", - "nopt": "^7.0.0", - "proc-log": "^4.1.0", + "make-fetch-happen": "^15.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", "semver": "^7.3.5", - "tar": "^6.2.1", - "which": "^4.0.0" + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/node-gyp-build": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", - "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", - "dev": true, - "license": "MIT", - "optional": true, - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/node-gyp-build-optional-packages": { @@ -10334,6 +10791,7 @@ "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "detect-libc": "^2.0.1" }, @@ -10343,67 +10801,20 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, - "node_modules/node-gyp/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/node-gyp/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/node-gyp/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.2.tgz", + "integrity": "sha512-mIcis6w+JiQf3P7t7mg/35GKB4T1FQsBOtMIvuKw4YErj5RjtbhcTd5/I30fmkmGMwvI0WlzSNN+27K0QCMkAw==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=20" } }, "node_modules/node-gyp/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", "dev": true, "license": "ISC", "dependencies": { @@ -10413,45 +10824,38 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true, + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "license": "MIT" }, + "node_modules/nodemailer": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.0.tgz", + "integrity": "sha512-xvVJf/f0bzmNpnRIbhCp/IKxaHgJ6QynvUbLXzzMRPG3LDQr5oXkYuw4uDFyFYs8cge8agwwrJAXZsd4hhMquw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nopt": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", - "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", "dev": true, "license": "ISC", "dependencies": { - "abbrev": "^2.0.0" + "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/normalize-package-data": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", - "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^7.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/normalize-path": { @@ -10464,128 +10868,106 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-bundled": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", - "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "node_modules/npm-bundled": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", + "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", "dev": true, "license": "ISC", "dependencies": { - "npm-normalize-package-bin": "^3.0.0" + "npm-normalize-package-bin": "^5.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-install-checks": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", - "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", + "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "semver": "^7.1.1" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-normalize-package-bin": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", - "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", "dev": true, "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-package-arg": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", - "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", + "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", "dev": true, "license": "ISC", "dependencies": { - "hosted-git-info": "^7.0.0", - "proc-log": "^4.0.0", + "hosted-git-info": "^9.0.0", + "proc-log": "^6.0.0", "semver": "^7.3.5", - "validate-npm-package-name": "^5.0.0" + "validate-npm-package-name": "^7.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-packlist": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", - "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.3.tgz", + "integrity": "sha512-zPukTwJMOu5X5uvm0fztwS5Zxyvmk38H/LfidkOMt3gbZVCyro2cD/ETzwzVPcWZA3JOyPznfUN/nkyFiyUbxg==", "dev": true, "license": "ISC", "dependencies": { - "ignore-walk": "^6.0.4" + "ignore-walk": "^8.0.0", + "proc-log": "^6.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-pick-manifest": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.1.0.tgz", - "integrity": "sha512-nkc+3pIIhqHVQr085X9d2JzPzLyjzQS96zbruppqC9aZRm/x8xx6xhI98gHtsfELP2bE+loHq8ZaHFHhe+NauA==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", + "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", "dev": true, "license": "ISC", "dependencies": { - "npm-install-checks": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0", - "npm-package-arg": "^11.0.0", + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", "semver": "^7.3.5" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-registry-fetch": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-17.1.0.tgz", - "integrity": "sha512-5+bKQRH0J1xG1uZ1zMNvxW0VEyoNWgJpY9UDuluPFLKDfJ9u2JmmjmTJV1srBGQOROfdBMiVvnH2Zvpbm+xkVA==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", + "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/redact": "^2.0.0", + "@npmcli/redact": "^4.0.0", "jsonparse": "^1.3.1", - "make-fetch-happen": "^13.0.0", + "make-fetch-happen": "^15.0.0", "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", - "minizlib": "^2.1.2", - "npm-package-arg": "^11.0.0", - "proc-log": "^4.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" + "minipass-fetch": "^5.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" }, "engines": { - "node": ">=8" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/nth-check": { @@ -10605,17 +10987,15 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "dev": true, + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -10624,18 +11004,21 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT" }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -10645,10 +11028,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -10658,12 +11040,20 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -10680,232 +11070,139 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/open": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", - "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "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": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" + "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": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.8.0" } }, "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", + "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", "dev": true, "license": "MIT", "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.2.2", + "string-width": "^8.1.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=10" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/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==", + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ora/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/ora/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/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/ora/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/ora/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/ora/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/ora/node_modules/string-width": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz", + "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==", "dev": true, "license": "MIT", "dependencies": { - "mimic-fn": "^2.1.0" + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=6" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, "license": "MIT", "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ora/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" + "node": ">=12" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/ordered-binary": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.1.tgz", - "integrity": "sha512-5VyHfHY3cd0iza71JepYG50My+YUbrFtGoUz2ooEydPyPM7Aai/JW098juLr+RG6+rDJuzNNTsEQu2DZa1A41A==", - "dev": true, - "license": "MIT" - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", + "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "optional": true }, "node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "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": "^1.0.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "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": { - "aggregate-error": "^3.0.0" + "p-limit": "^3.0.2" }, "engines": { "node": ">=10" @@ -10914,71 +11211,56 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-retry": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.0.tgz", - "integrity": "sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==", + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", "dev": true, "license": "MIT", - "dependencies": { - "@types/retry": "0.12.2", - "is-network-error": "^1.0.0", - "retry": "^0.13.1" - }, "engines": { - "node": ">=16.17" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-retry/node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/pacote": { - "version": "18.0.6", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-18.0.6.tgz", - "integrity": "sha512-+eK3G27SMwsB8kLIuj4h1FUhHtwiEUo21Tw8wNjmvdlpOEr613edv+8FUsTj/4F/VN5ywGE19X18N7CC2EJk6A==", + "version": "21.0.4", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.4.tgz", + "integrity": "sha512-RplP/pDW0NNNDh3pnaoIWYPvNenS7UqMbXyvMqJczosiFWTeGGwJC2NQBLqKf4rGLFfwCOnntw1aEp9Jiqm1MA==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/git": "^5.0.0", - "@npmcli/installed-package-contents": "^2.0.1", - "@npmcli/package-json": "^5.1.0", - "@npmcli/promise-spawn": "^7.0.0", - "@npmcli/run-script": "^8.0.0", - "cacache": "^18.0.0", + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", - "npm-package-arg": "^11.0.0", - "npm-packlist": "^8.0.0", - "npm-pick-manifest": "^9.0.0", - "npm-registry-fetch": "^17.0.0", - "proc-log": "^4.0.0", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", "promise-retry": "^2.0.1", - "sigstore": "^2.2.0", - "ssri": "^10.0.0", - "tar": "^6.1.11" + "sigstore": "^4.0.0", + "ssri": "^13.0.0", + "tar": "^7.4.3" }, "bin": { "pacote": "bin/index.js" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/parent-module": { @@ -10994,101 +11276,88 @@ "node": ">=6" } }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" + "entities": "^6.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse-json/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/parse-node-version": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", - "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "node_modules/parse5-html-rewriting-stream": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.0.tgz", + "integrity": "sha512-wzh11mj8KKkno1pZEu+l2EVeWsuKDfR5KNWZOTsslfUX8lPDZx77m9T0kIoAVkFtD1nx6YF8oh4BnPHvxMtNMw==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "devOptional": true, - "license": "MIT", "dependencies": { - "entities": "^4.4.0" + "entities": "^6.0.0", + "parse5": "^8.0.0", + "parse5-sax-parser": "^8.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5-html-rewriting-stream": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", - "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "node_modules/parse5-html-rewriting-stream/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^4.3.0", - "parse5": "^7.0.0", - "parse5-sax-parser": "^7.0.0" + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" }, "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/parse5-sax-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", - "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", + "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==", "dev": true, "license": "MIT", "dependencies": { - "parse5": "^7.0.0" + "parse5": "^8.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "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": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=8" } }, "node_modules/path-is-absolute": { @@ -11119,110 +11388,215 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-type": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", - "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", - "dev": true, + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "license": "MIT", - "engines": { - "node": ">=12" - }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, - "license": "ISC" + "license": "MIT" }, - "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, + "node_modules/pg": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.11.0", + "pg-pool": "^3.11.0", + "pg-protocol": "^1.11.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, "engines": { - "node": ">=12" + "node": ">= 16.0.0" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "license": "MIT", - "optional": true, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz", + "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", "engines": { - "node": ">=6" + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", + "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/piscina": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.6.1.tgz", - "integrity": "sha512-z30AwWGtQE+Apr+2WBZensP2lIvwoaMcOPkQlIEmSGMJNUvaYACylPYrQM6wSdUNJlnDVMSpLv7xTMJqlVshOA==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.4.tgz", + "integrity": "sha512-7uU4ZnKeQq22t9AsmHGD2w4OYQGonwFnTypDypaWi7Qr2EvQIFVtG8J5D/3bE7W123Wdc9+v4CZDu5hJXVCtBg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=20.x" + }, "optionalDependencies": { - "nice-napi": "^1.0.2" + "@napi-rs/nice": "^1.0.4" } }, - "node_modules/pkg-dir": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", - "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", "dev": true, "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/playwright": { + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.1.tgz", + "integrity": "sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "find-up": "^6.3.0" + "playwright-core": "1.58.1" + }, + "bin": { + "playwright": "cli.js" }, "engines": { - "node": ">=14.16" + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.1.tgz", + "integrity": "sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" } }, "node_modules/postcss": { - "version": "8.4.41", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -11240,46 +11614,14 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-loader": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", - "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cosmiconfig": "^9.0.0", - "jiti": "^1.20.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, "node_modules/postcss-media-query-parser": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", @@ -11287,114 +11629,65 @@ "dev": true, "license": "MIT" }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "dev": true, - "license": "ISC", + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">=4" } }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", - "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", - "dev": true, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", "license": "MIT", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">=0.10.0" } }, - "node_modules/postcss-modules-scope": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", - "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">=0.10.0" } }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "license": "ISC", + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", "dependencies": { - "icss-utils": "^5.0.0" + "xtend": "^4.0.0" }, "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">=0.10.0" } }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "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", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, "engines": { - "node": ">=4" + "node": ">= 0.8.0" } }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, "node_modules/proc-log": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", - "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true, - "license": "ISC" - }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", @@ -11413,7 +11706,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -11423,23 +11715,11 @@ "node": ">= 0.10" } }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", - "dev": true, - "license": "MIT", - "optional": true + "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": "1.4.1", @@ -11459,13 +11739,12 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -11474,68 +11753,38 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } + "node_modules/race-for-federica-dashboard": { + "resolved": "client", + "link": true }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dev": true, + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -11547,162 +11796,68 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">= 14.18.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "dev": true, "license": "Apache-2.0" }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", - "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2" - }, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true, - "license": "MIT" - }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.4" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/regex-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", - "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true, "license": "MIT" }, - "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" }, "engines": { - "node": ">=4" - } - }, - "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11718,46 +11873,14 @@ "node": ">=4" } }, - "node_modules/resolve-url-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", - "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "adjust-sourcemap-loader": "^4.0.0", - "convert-source-map": "^1.7.0", - "loader-utils": "^2.0.0", - "postcss": "^8.2.14", - "source-map": "0.6.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/resolve-url-loader/node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/resolve-url-loader/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, "node_modules/restore-cursor": { @@ -11787,17 +11910,6 @@ "node": ">= 4" } }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -11806,99 +11918,156 @@ "license": "MIT" }, "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^7.1.3" + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" }, "bin": { - "rimraf": "bin.js" + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rollup": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz", - "integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==", + "node_modules/rimraf/node_modules/glob": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", + "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.2", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-beta.58", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.58.tgz", + "integrity": "sha512-v1FCjMZCan7f+xGAHBi+mqiE4MlH7I+SXEHSQSJoMOGNNB2UYtvMiejsq9YuUOiZjNeUeV/a21nSFbrUR+4ZCQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@oxc-project/types": "=0.106.0", + "@rolldown/pluginutils": "1.0.0-beta.58" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.20.0", - "@rollup/rollup-android-arm64": "4.20.0", - "@rollup/rollup-darwin-arm64": "4.20.0", - "@rollup/rollup-darwin-x64": "4.20.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.20.0", - "@rollup/rollup-linux-arm-musleabihf": "4.20.0", - "@rollup/rollup-linux-arm64-gnu": "4.20.0", - "@rollup/rollup-linux-arm64-musl": "4.20.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.20.0", - "@rollup/rollup-linux-riscv64-gnu": "4.20.0", - "@rollup/rollup-linux-s390x-gnu": "4.20.0", - "@rollup/rollup-linux-x64-gnu": "4.20.0", - "@rollup/rollup-linux-x64-musl": "4.20.0", - "@rollup/rollup-win32-arm64-msvc": "4.20.0", - "@rollup/rollup-win32-ia32-msvc": "4.20.0", - "@rollup/rollup-win32-x64-msvc": "4.20.0", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.0.0-beta.58", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.58", + "@rolldown/binding-darwin-x64": "1.0.0-beta.58", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.58", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.58", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.58", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.58", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.58", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.58", + "@rolldown/binding-openharmony-arm64": "1.0.0-beta.58", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.58", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.58", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.58" } }, - "node_modules/run-applescript": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", - "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "dev": true, "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, "engines": { - "node": ">=18" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "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" - } - ], + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", "dependencies": { - "queue-microtask": "^1.2.2" + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" } }, "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" @@ -11908,7 +12077,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -11925,22 +12093,48 @@ ], "license": "MIT" }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, "license": "MIT" }, "node_modules/sass": { - "version": "1.77.6", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", - "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", "dev": true, "license": "MIT", "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", + "chokidar": "^4.0.0", + "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -11948,385 +12142,178 @@ }, "engines": { "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" } }, - "node_modules/sass-loader": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.0.tgz", - "integrity": "sha512-n13Z+3rU9A177dk4888czcVFiC8CL9dii4qpXWUg3YIIgZEvi9TCFKjOQcbK0kJM7DJu9VucrZFddvNfYCPwtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "neo-async": "^2.6.2" + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" }, "engines": { - "node": ">= 18.12.0" + "node": ">= 18" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "webpack": { - "optional": true - } + "url": "https://opencollective.com/express" } }, - "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", - "dev": true, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://opencollective.com/express" } }, - "node_modules/schema-utils/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "node_modules/server": { + "resolved": "server", + "link": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "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": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" + "shebang-regex": "^3.0.0" }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } + "engines": { + "node": ">=8" } }, - "node_modules/select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "dev": true, - "license": "MIT" - }, - "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "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", - "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" - }, "engines": { - "node": ">=10" + "node": ">=8" } }, - "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/send/node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/send/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/send/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-index/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" + "node": ">= 0.4" }, - "engines": { - "node": ">= 0.6" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true, - "license": "ISC" - }, - "node_modules/serve-index/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/serve-index/node_modules/setprototypeof": { + "node_modules/side-channel": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dev": true, + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", "dependencies": { - "define-data-property": "^1.1.4", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" + "object-inspect": "^1.13.3" }, "engines": { "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.2" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "license": "MIT", "dependencies": { - "shebang-regex": "^3.0.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" }, "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/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", - "dev": true, - "license": "MIT", + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -12335,6 +12322,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -12349,64 +12343,67 @@ } }, "node_modules/sigstore": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.3.1.tgz", - "integrity": "sha512-8G+/XDU8wNsJOQS5ysDVO0Etg9/2uA5gR9l4ZwijjlwxBcrU6RPfwi2+jJmbP+Ap1Hlp/nVAaEO4Fj22/SL2gQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.0.tgz", + "integrity": "sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^2.3.2", - "@sigstore/core": "^1.0.0", - "@sigstore/protobuf-specs": "^0.3.2", - "@sigstore/sign": "^2.3.2", - "@sigstore/tuf": "^2.3.4", - "@sigstore/verify": "^1.2.1" + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.1.0", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.1.0", + "@sigstore/tuf": "^4.0.1", + "@sigstore/verify": "^3.1.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "dev": true, "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/smart-buffer": { @@ -12421,17 +12418,17 @@ } }, "node_modules/socket.io": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", - "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.5.2", + "debug": "~4.4.1", + "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" }, @@ -12440,50 +12437,85 @@ } }, "node_modules/socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" + "debug": "~4.4.1", + "ws": "~8.18.3" } }, "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", "dev": true, "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" + "debug": "~4.4.1" }, "engines": { "node": ">=10.0.0" } }, - "node_modules/sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/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==", "dev": true, "license": "MIT", "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "dev": true, "license": "MIT", "dependencies": { - "ip-address": "^9.0.5", + "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -12492,13 +12524,13 @@ } }, "node_modules/socks-proxy-agent": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", - "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.1.1", + "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" }, @@ -12507,59 +12539,25 @@ } }, "node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", "engines": { - "node": ">= 8" + "node": ">=0.10.0" } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "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/source-map-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", - "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "^0.6.3", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.72.1" - } - }, - "node_modules/source-map-loader/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -12571,16 +12569,6 @@ "source-map": "^0.6.0" } }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -12611,72 +12599,77 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.20", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", - "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", "dev": true, "license": "CC0-1.0" }, - "node_modules/spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/ssri": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", + "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" + "minipass": "^7.0.3" }, "engines": { - "node": ">=6.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" + "engines": { + "node": "*" } }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, - "license": "BSD-3-Clause" + "license": "MIT" }, - "node_modules/ssri": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", - "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">= 0.8" } }, - "node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/streamroller": { @@ -12698,32 +12691,12 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } }, "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", @@ -12738,52 +12711,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -12797,1762 +12724,896 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "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", - "dependencies": { - "ansi-regex": "^5.0.1" - }, "engines": { "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/symbol-observable": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", - "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dev": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "license": "ISC", "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" }, "engines": { - "node": ">=8" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=8" + "node": ">=14.18.0" } }, - "node_modules/tar/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", "dev": true, "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/terser": { - "version": "5.31.6", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", - "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", - "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" }, "engines": { - "node": ">=10" + "node": ">=14.18.0" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/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/terser-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/terser-webpack-plugin/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/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/thingies": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", - "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", - "dev": true, - "license": "Unlicense", - "engines": { - "node": ">=10.18" - }, - "peerDependencies": { - "tslib": "^2" - } - }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "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/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tree-dump": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", - "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "license": "0BSD" - }, - "node_modules/tuf-js": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.1.tgz", - "integrity": "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tufjs/models": "2.0.1", - "debug": "^4.3.4", - "make-fetch-happen": "^13.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typed-assert": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", - "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", - "dev": true, - "license": "MIT" - }, - "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ua-parser-js": { - "version": "0.7.38", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.38.tgz", - "integrity": "sha512-fYmIy7fKTSFAhG3fuPlubeGaMoAd6r0rSnfEsO5nEY55i26KSLt9EH7PLQiiqPUhNqYIJvSkTy1oArIcXAbPbA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unique-filename": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", - "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", - "dev": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^4.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/unique-slug": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", - "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", - "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.1.2", - "picocolors": "^1.0.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/uri-js/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/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/validate-npm-package-name": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", - "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vite": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz", - "integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.40", - "rollup": "^4.13.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], + "node_modules/tar": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "BlueOak-1.0.0", "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, "engines": { - "node": ">=12" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=14.0.0" } }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=14.14" } }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], + "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", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "is-number": "^7.0.0" + }, "engines": { - "node": ">=12" + "node": ">=8.0" } }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], "engines": { - "node": ">=12" + "node": ">=0.6" } }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" + "bin": { + "tree-kill": "cli.js" } }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=12" + "node": ">= 14.0.0" } }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=12" + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } } }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, - "hasInstallScript": true, "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, "bin": { - "esbuild": "bin/esbuild" + "tsx": "dist/cli.mjs" }, "engines": { - "node": ">=12" + "node": ">=18.0.0" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "fsevents": "~2.3.3" } }, - "node_modules/void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "node_modules/tsx/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": ">=0.10.0" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "node_modules/tuf-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", + "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", "dev": true, "license": "MIT", "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" }, "engines": { - "node": ">=10.13.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "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": { - "minimalistic-assert": "^1.0.0" + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { - "defaults": "^1.0.3" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" } }, - "node_modules/weak-lru-cache": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", - "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack": { - "version": "5.93.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", - "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" - }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } + "engines": { + "node": ">=14.17" } }, - "node_modules/webpack-dev-middleware": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.3.0.tgz", - "integrity": "sha512-xD2qnNew+F6KwOGZR7kWdbIou/ud7cVqLEXeK1q0nHcNsX/u7ul/fSdlOTX4ntSL5FNFy7ZJJXbf0piF591JYw==", + "node_modules/typescript-eslint": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", + "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", "dev": true, "license": "MIT", "dependencies": { - "colorette": "^2.0.10", - "memfs": "^4.6.0", - "mime-types": "^2.1.31", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" + "@typescript-eslint/eslint-plugin": "8.54.0", + "@typescript-eslint/parser": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0" }, "engines": { - "node": ">= 18.12.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/webpack-dev-server": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz", - "integrity": "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/bonjour": "^3.5.13", - "@types/connect-history-api-fallback": "^1.5.4", - "@types/express": "^4.17.21", - "@types/serve-index": "^1.9.4", - "@types/serve-static": "^1.15.5", - "@types/sockjs": "^0.3.36", - "@types/ws": "^8.5.10", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.2.1", - "chokidar": "^3.6.0", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", - "graceful-fs": "^4.2.6", - "html-entities": "^2.4.0", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.1.0", - "launch-editor": "^2.6.1", - "open": "^10.0.3", - "p-retry": "^6.2.0", - "rimraf": "^5.0.5", - "schema-utils": "^4.2.0", - "selfsigned": "^2.4.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^7.1.0", - "ws": "^8.16.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true + "node_modules/ua-parser-js": { + "version": "0.7.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz", + "integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" }, - "webpack-cli": { - "optional": true + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" } - } - }, - "node_modules/webpack-dev-server/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, + ], "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/webpack-dev-server/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, "bin": { - "glob": "dist/esm/bin.mjs" + "ua-parser-js": "script/cli.js" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": "*" } }, - "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "node_modules/undici": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.20.0.tgz", + "integrity": "sha512-MJZrkjyd7DeC+uPZh+5/YaMDxFiiEEaDgbUSVMXayofAkDWF1088CDo+2RPg7B1BuS1qf1vgNE7xqwPxE0DuSQ==", "dev": true, "license": "MIT", - "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } + "node": ">=20.18.1" } }, - "node_modules/webpack-dev-server/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz", + "integrity": "sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "unique-slug": "^6.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/webpack-dev-server/node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "node_modules/unique-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-6.0.0.tgz", + "integrity": "sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==", "dev": true, "license": "ISC", "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" + "imurmurhash": "^0.1.4" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/webpack-merge": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", - "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.1" - }, "engines": { - "node": ">=18.0.0" + "node": ">= 4.0.0" } }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "dev": true, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", "engines": { - "node": ">=10.13.0" + "node": ">= 0.8" } }, - "node_modules/webpack-subresource-integrity": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", - "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", - "dev": true, + "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==", + "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": { - "typed-assert": "^1.0.8" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, - "engines": { - "node": ">= 12" + "bin": { + "update-browserslist-db": "cli.js" }, "peerDependencies": { - "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", - "webpack": "^5.12.0" - }, - "peerDependenciesMeta": { - "html-webpack-plugin": { - "optional": true - } + "browserslist": ">= 4.21.0" } }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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": "MIT", + "license": "BSD-2-Clause", "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" + "punycode": "^2.1.0" } }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "node_modules/uri-js/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", - "peerDependencies": { - "ajv": "^6.9.1" + "engines": { + "node": ">=6" } }, - "node_modules/webpack/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack/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, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true, "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">= 0.4.0" } }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "license": "Apache-2.0", "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" } }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", "dev": true, - "license": "Apache-2.0", + "license": "ISC", "engines": { - "node": ">=0.8.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "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.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "isexe": "^2.0.0" + "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": { - "which": "bin/which" + "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/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "node_modules/vite/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", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=8" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" }, "engines": { - "node": ">=10" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } } }, - "node_modules/wrap-ansi-cjs/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==", + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=0.10.0" } }, - "node_modules/wrap-ansi-cjs/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==", + "node_modules/watchpack": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.0.tgz", + "integrity": "sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" }, "engines": { - "node": ">=7.0.0" + "node": ">=10.13.0" } }, - "node_modules/wrap-ansi-cjs/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/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true }, - "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "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": "MIT", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, "engines": { - "node": ">=8" + "node": ">= 8" } }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" }, "engines": { "node": ">=8" } }, - "node_modules/wrap-ansi/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, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">= 12.0.0" } }, - "node_modules/wrap-ansi/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, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" }, "engines": { - "node": ">=7.0.0" + "node": ">= 12.0.0" } }, - "node_modules/wrap-ansi/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/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" + "node_modules/winston/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "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": ">=8" + "node": ">=0.10.0" } }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "license": "MIT", "engines": { @@ -14571,11 +13632,19 @@ } } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -14585,7 +13654,6 @@ "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/yargs": { @@ -14617,55 +13685,46 @@ "node": ">=12" } }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "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", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yocto-queue": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", - "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", "dev": true, "license": "MIT", "engines": { - "node": ">=12.20" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/yoctocolors-cjs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", - "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", "dev": true, "license": "MIT", "engines": { @@ -14675,11 +13734,91 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + }, "node_modules/zone.js": { - "version": "0.14.10", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz", - "integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.0.tgz", + "integrity": "sha512-LqLPpIQANebrlxY6jKcYKdgN5DTXyyHAKnnWWjE5pPfEQ4n7j5zn7mOEEpwNZVKGqx3kKKmvplEmoBrvpgROTA==", + "license": "MIT" + }, + "server": { + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@f123dashboard/shared": "file:../shared", + "@types/bcrypt": "^6.0.0", + "@types/jsonwebtoken": "^9.0.10", + "axios": "^1.8.4", + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.1", + "node-cron": "^4.2.1", + "nodemailer": "^8.0.0", + "pg": "^8.13.1", + "winston": "^3.18.3" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.5", + "@types/morgan": "^1.9.10", + "@types/node": "^22.19.9", + "@types/node-cron": "^3.0.11", + "@types/nodemailer": "^7.0.4", + "@types/pg": "^8.15.6", + "@types/supertest": "^6.0.3", + "supertest": "^7.2.2", + "ts-node": "^10.9.2", + "tsx": "^4.20.6", + "typescript": "^5.6.3", + "vitest": "^4.0.17" + } + }, + "server/node_modules/@types/node": { + "version": "22.19.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.9.tgz", + "integrity": "sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "server/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, "license": "MIT" + }, + "shared": { + "name": "@f123dashboard/shared", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "rimraf": "^6.0.1", + "typescript": "^5.7.2" + } } } } diff --git a/package.json b/package.json index 4e828dc4c..206a30913 100644 --- a/package.json +++ b/package.json @@ -1,66 +1,39 @@ { - "name": "coreui-free-angular-admin-template", - "version": "5.2.16", - "copyright": "Copyright 2024 creativeLabs Łukasz Holeczek", - "license": "MIT", - "author": "The CoreUI Team (https://github.com/orgs/coreui/people) and contributors", - "homepage": "https://coreui.io/angular", - "config": { - "theme": "default", - "coreui_library_short_version": "5.2", - "coreui_library_docs_url": "https://coreui.io/angular/docs/" - }, - "scripts": { - "ng": "ng", - "start": "ng serve -o", - "build": "ng build", - "watch": "ng build --watch --configuration development", - "test": "ng test" - }, + "name": "f123dashboard", + "version": "2.0.0", "private": true, + "type": "module", "dependencies": { - "@angular/animations": "^18.2.1", - "@angular/cdk": "^18.2.1", - "@angular/common": "^18.2.1", - "@angular/compiler": "^18.2.1", - "@angular/core": "^18.2.1", - "@angular/forms": "^18.2.1", - "@angular/language-service": "^18.2.1", - "@angular/platform-browser": "^18.2.1", - "@angular/platform-browser-dynamic": "^18.2.1", - "@angular/router": "^18.2.1", - "@coreui/angular": "~5.2.16", - "@coreui/angular-chartjs": "~5.2.16", - "@coreui/chartjs": "~4.0.0", - "@coreui/coreui": "~5.1.2", - "@coreui/icons": "^3.0.1", - "@coreui/icons-angular": "~5.2.16", - "@coreui/utils": "^2.0.2", - "chart.js": "^4.4.4", - "lodash-es": "^4.17.21", - "ngx-scrollbar": "^13.0.3", - "rxjs": "~7.8.1", - "tslib": "^2.7.0", - "zone.js": "~0.14.10" + "@coreui/coreui": "~5.5.0", + "g": "^2.0.1", + "nodemailer": "^8.0.0" }, "devDependencies": { - "@angular-devkit/build-angular": "^18.2.1", - "@angular/cli": "^18.2.1", - "@angular/compiler-cli": "^18.2.1", - "@angular/localize": "^18.2.1", - "@types/jasmine": "^5.1.4", - "@types/lodash-es": "^4.17.12", - "@types/node": "^20.16.1", - "jasmine-core": "^5.2.0", - "karma": "^6.4.4", - "karma-chrome-launcher": "^3.2.0", - "karma-coverage": "^2.2.1", - "karma-jasmine": "^5.1.0", - "karma-jasmine-html-reporter": "^2.1.0", - "typescript": "~5.5.4" + "@playwright/test": "^1.54.2", + "@types/node": "^25.2.1", + "concurrently": "^9.2.1", + "sass": "^1.97.0" + }, + "overrides": { + "sass": "^1.97.0" + }, + "scripts": { + "start:client": "npm start --prefix client", + "build:client": "npm run build --prefix client", + "test:client": "npm test --prefix client", + "start:server": "npm run dev --prefix server", + "build:server": "npm run build --prefix server", + "build:shared": "npm run build --prefix shared", + "build": "npm run clean && npm run build:all && npm run copy:all", + "build:all": "npm run build:shared && npm run build:server && npm run build:client", + "clean": "node scripts/clean.js", + "copy:all": "node scripts/copy-to-dist.js", + "start:prod": "node dist/server/server.js", + "dev": "concurrently \"npm run build:shared -- --watch\" \"npm run start:server\" \"npm run start:client\"" }, - "engines": { - "node": "^18.19.0 || ^20.9.0", - "npm": ">= 9" - } + "workspaces": [ + "client", + "server", + "shared" + ] } diff --git a/scripts/clean.js b/scripts/clean.js new file mode 100644 index 000000000..f3d058405 --- /dev/null +++ b/scripts/clean.js @@ -0,0 +1,8 @@ +import { rmSync } from 'fs'; + +try { + rmSync('../dist', { recursive: true, force: true }); + console.log('✅ Cleaned dist/ directory'); +} catch (err) { + console.log('ℹ️ No dist/ directory to clean'); +} diff --git a/scripts/copy-to-dist.js b/scripts/copy-to-dist.js new file mode 100644 index 000000000..839d0f82d --- /dev/null +++ b/scripts/copy-to-dist.js @@ -0,0 +1,134 @@ +import { cpSync, mkdirSync, writeFileSync, existsSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const rootDir = join(__dirname, '..'); +const distDir = join(__dirname, '..', '..', 'dist'); + +console.log('📦 Copying files to ../dist/...'); + +// Create dist directory +mkdirSync(distDir, { recursive: true }); + +// Copy shared +console.log(' → Copying shared types...'); +if (existsSync(join(rootDir, 'shared/dist'))) { + cpSync( + join(rootDir, 'shared/dist'), + join(distDir, 'shared'), + { recursive: true } + ); +} else { + console.warn(' ⚠️ shared/dist not found, skipping...'); +} + +// Copy server +console.log(' → Copying server...'); +if (existsSync(join(rootDir, 'server/dist'))) { + cpSync( + join(rootDir, 'server/dist'), + join(distDir, 'server'), + { recursive: true } + ); +} else { + console.warn(' ⚠️ server/dist not found, skipping...'); +} + +// Copy client +console.log(' → Copying client...'); +if (existsSync(join(rootDir, 'client/dist/browser'))) { + cpSync( + join(rootDir, 'client/dist/browser'), + join(distDir, 'client/browser'), + { recursive: true } + ); +} else { + console.warn(' ⚠️ client/dist/browser not found, skipping...'); +} + + +// REMOVED: Copying node_modules is unnecessary and buggy with workspaces. +// Node will resolve dependencies from the root node_modules folder. + +// Create production package.json (Optional, mostly for reference now) +console.log(' → Creating production package.json...'); +// Create production package.json (Optional, mostly for reference now) +console.log(' → Creating production package.json...'); +const prodPackage = { + name: "f123dashboard-server", + version: "1.0.0", + type: "module", + scripts: { + start: "node server/server.js" + }, + dependencies: { + "express": "^4.18.2", + "pg": "^8.11.3", + "bcrypt": "^5.1.1", + "jsonwebtoken": "^9.0.2", + "dotenv": "^16.3.1", + "winston": "^3.11.0", + "winston-daily-rotate-file": "^4.7.1", + "morgan": "^1.10.0", + "cors": "^2.8.5", + "helmet": "^7.1.0", + "compression": "^1.7.4", + "express-rate-limit": "^7.1.5", + "nodemailer": "^6.9.7", + "node-cron": "^3.0.3", + "axios": "^1.8.4" + } +}; + +writeFileSync( + join(distDir, 'package.json'), + JSON.stringify(prodPackage, null, 2) +); + +// Create .env template +console.log(' → Creating .env template...'); +const envTemplate = `# Database +RACEFORFEDERICA_DB_DATABASE_URL=postgresql://user:pass@localhost:5432/f123dashboard + +# Authentication +JWT_SECRET=your-production-secret-key-min-32-chars + +# Email +MAIL_USER=noreply@yourdomain.com +MAIL_PASS=your-email-password + +# Twitch +RACEFORFEDERICA_DREANDOS_SECRET=your-twitch-webhook-secret + +# Server +PORT=3000 +NODE_ENV=production +LOG_LEVEL=info +`; + +writeFileSync(join(distDir, '.env.example'), envTemplate); + + +console.log('\n✅ Build copied to dist/'); +console.log('\n📁 Dist structure:'); +console.log('dist/'); +console.log('├── package.json'); +console.log('├── .env.example'); +console.log('├── README.md'); +console.log('├── node_modules/'); +console.log('├── shared/'); +console.log('├── server/'); +console.log('│ ├── server.js'); +console.log('│ ├── controllers/'); +console.log('│ ├── services/'); +console.log('│ ├── routes/'); +console.log('│ ├── middleware/'); +console.log('│ └── config/'); +console.log('└── client/'); +console.log(' └── browser/'); +console.log(' └── index.html'); +console.log('\n🚀 Ready for deployment!'); +console.log(' Run: npm start:prod (for local testing)'); +console.log(' Or copy dist/ folder to your production server'); diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 000000000..e6464093d --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# Stop on any error +set -e + +# Log function +log() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" +} + +log "🚀 Starting deployment..." + +# Navigate to project root (adjust path if necessary) +cd /var/www/rff/f123dashboard || { log "❌ Failed to navigate to project directory"; exit 1; } + +log "⬇️ Pulling latest changes..." +#git pull origin main +git pull origin server-migration + +log "📦 Installing dependencies..." +NODE_ENV=development npm install + +log "🏗️ Building project..." +npm run build + +log "Installing dist dependencies..." +cd /var/www/rff/dist || { log "❌ Failed to navigate to dist directory"; exit 1; } +npm install + +log "🔄 Restarting application..." +pm2 restart rff --update-env + +log "✅ Deployment complete!" diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 000000000..4c0fcad5a --- /dev/null +++ b/server/.env.example @@ -0,0 +1,20 @@ +# Database Configuration +RACEFORFEDERICA_DB_DATABASE_URL=postgresql://username:password@localhost:5432/raceforfederica_db + +# JWT Configuration +JWT_SECRET=your-very-secure-secret-key-here-change-in-production + +# Email Configuration (for Nodemailer) +MAIL_USER=your-email@example.com +MAIL_PASS=your-email-password + +# Twitch Configuration +RACEFORFEDERICA_DREANDOS_SECRET=your-twitch-secret-here + +# Server Configuration +PORT=3000 +NODE_ENV=development + +# Logging Configuration +# Levels: error, warn, info, http, debug +LOG_LEVEL=debug diff --git a/server/docs/auth-service.md b/server/docs/auth-service.md new file mode 100644 index 000000000..8c4317232 --- /dev/null +++ b/server/docs/auth-service.md @@ -0,0 +1,303 @@ +# AuthService Documentation + +## Overview +The `AuthService` class provides comprehensive authentication and session management functionality for the F123 Dashboard application. It handles user registration, login, password management, token validation, and session lifecycle management using JWT tokens and PostgreSQL database storage. + +## Features +- User registration with validation +- Secure login with password hashing +- JWT token generation and validation +- Session management with expiration +- Password change functionality +- Token refresh mechanism +- Session cleanup and logout +- User agent tracking +- Account activation/deactivation + +## Database Tables Used +- `fanta_player` - User account information +- `user_sessions` - Active user sessions + +## Configuration +The service requires the following environment variables: +- `RACEFORFEDERICA_DB_DATABASE_URL` - PostgreSQL connection string +- `JWT_SECRET` - Secret key for JWT token signing + +## Constants +- `TOKEN_EXPIRY_DAYS = 7` - Session expiration in days +- `TOKEN_EXPIRY_JWT = '7d'` - JWT token expiration + +## Methods + +### Constructor +```typescript +constructor() +``` +Initializes the PostgreSQL connection pool and validates required environment variables. + +**Throws:** +- `Error` - If JWT_SECRET environment variable is not set + +### Public Methods + +#### `login(username: string, password: string, userAgent?: string): Promise` +Authenticates a user and creates a new session. + +**Parameters:** +- `username` - User's username +- `password` - User's password (plain text) +- `userAgent` - Optional user agent string + +**Returns:** +- JSON string containing: + - `success: boolean` - Operation success status + - `message: string` - Success/error message + - `user: object` - User information (if successful) + - `token: string` - JWT token (if successful) + +**Validation:** +- Checks if username and password are provided +- Validates user exists and is active +- Verifies password hash + +**Example Response:** +```json +{ + "success": true, + "message": "Login successful", + "user": { + "id": 1, + "username": "john_doe", + "name": "John", + "surname": "Doe" + }, + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} +``` + +#### `register(username: string, name: string, surname: string, password: string, userAgent?: string): Promise` +Registers a new user account. + +**Parameters:** +- `username` - Desired username (3-30 characters, alphanumeric + underscore) +- `name` - User's first name +- `surname` - User's last name +- `password` - Password (minimum 8 characters with complexity requirements) +- `userAgent` - Optional user agent string + +**Returns:** +- JSON string with registration result and JWT token + +**Password Requirements:** +- Minimum 8 characters +- Must contain uppercase letter +- Must contain lowercase letter +- Must contain number +- Must contain special character + +**Username Requirements:** +- 3-30 characters +- Only alphanumeric characters and underscores +- Must be unique + +#### `validateToken(jwtToken: string): Promise` +Validates a JWT token and returns user information. + +**Parameters:** +- `jwtToken` - JWT token to validate + +**Returns:** +- JSON string containing: + - `valid: boolean` - Token validity + - `userId: number` - User ID (if valid) + - `username: string` - Username (if valid) + +#### `changePassword(jwtToken: string, currentPassword: string, newPassword: string): Promise` +Changes user's password after validating current password. + +**Parameters:** +- `jwtToken` - Valid JWT token +- `currentPassword` - Current password +- `newPassword` - New password (must meet complexity requirements) + +**Returns:** +- JSON string with operation result + +**Validation:** +- Validates JWT token +- Verifies current password +- Ensures new password meets requirements +- Prevents reusing current password + +#### `refreshToken(oldJwtToken: string, userAgent?: string): Promise` +Refreshes an existing JWT token. + +**Parameters:** +- `oldJwtToken` - Current JWT token +- `userAgent` - Optional user agent string + +**Returns:** +- JSON string with new JWT token + +#### `logout(jwtToken: string): Promise` +Logs out the current session. + +**Parameters:** +- `jwtToken` - JWT token to invalidate + +**Returns:** +- JSON string with logout result + +#### `logoutAllSessions(jwtToken: string): Promise` +Logs out all sessions for the current user. + +**Parameters:** +- `jwtToken` - Valid JWT token + +**Returns:** +- JSON string with operation result + +#### `getUserSessions(jwtToken: string): Promise` +Retrieves all active sessions for the current user. + +**Parameters:** +- `jwtToken` - Valid JWT token + +**Returns:** +- JSON string with session information + +#### `cleanupExpiredSessions(): Promise` +Cleans up expired sessions from the database. + +**Returns:** +- JSON string with cleanup result + +**Maintenance:** +- Marks expired sessions as inactive +- Deletes sessions older than 30 days + +### Private Methods + +#### `hashPassword(password: string): string` +Hashes a password using SHA-256. + +#### `comparePassword(password: string, hashedPassword: string): boolean` +Compares a plain text password with a hashed password. + +#### `generateSessionToken(): string` +Generates a secure random session token. + +#### `generateJWTToken(userId: number, username: string): string` +Creates a JWT token with user information. + +#### `createSession(userId: number, userAgent?: string): Promise` +Creates a new session record and returns JWT token. + +#### `validateSession(jwtToken: string): Promise<{valid: boolean; userId?: number; username?: string}>` +Validates a session and updates last activity. + +#### `invalidateSession(jwtToken: string): Promise` +Invalidates all sessions for a user. + +#### `cleanExpiredSessions(): Promise` +Internal method to clean expired sessions. + +#### `sanitizeUserAgent(userAgent?: string): string | undefined` +Sanitizes user agent string for database storage. + +#### `validateLoginInput(username: string, password: string): void` +Validates login input parameters. + +#### `validateRegisterInput(username: string, name: string, surname: string, password: string): void` +Validates registration input parameters. + +#### `validateChangePasswordInput(currentPassword: string, newPassword: string): void` +Validates password change input parameters. + +## Error Handling +All public methods return JSON strings with error information rather than throwing exceptions. Common error scenarios: +- Invalid credentials +- Expired tokens +- Account disabled +- Username already exists +- Password requirements not met +- Database connection errors + +## Security Features +- Password hashing using SHA-256 +- JWT token expiration +- Session timeout (7 days) +- User agent tracking +- Account activation/deactivation +- Session invalidation on logout +- Automatic cleanup of expired sessions + +## Usage Example +```typescript +const authService = new AuthService(); + +// Register new user +const registerResult = await authService.register( + 'john_doe', + 'John', + 'Doe', + 'SecurePass123!', + 'Mozilla/5.0...' +); + +// Login +const loginResult = await authService.login( + 'john_doe', + 'SecurePass123!', + 'Mozilla/5.0...' +); + +// Validate token +const tokenResult = await authService.validateToken(jwtToken); + +// Change password +const changeResult = await authService.changePassword( + jwtToken, + 'SecurePass123!', + 'NewSecurePass456!' +); + +// Logout +const logoutResult = await authService.logout(jwtToken); +``` + +## Database Schema Requirements + +### fanta_player Table +```sql +CREATE TABLE fanta_player ( + id SERIAL PRIMARY KEY, + username VARCHAR(30) UNIQUE NOT NULL, + name VARCHAR(50) NOT NULL, + surname VARCHAR(50) NOT NULL, + password VARCHAR(255) NOT NULL, + image VARCHAR(255), + created_at TIMESTAMP DEFAULT NOW(), + last_login TIMESTAMP, + password_updated_at TIMESTAMP DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE +); +``` + +### user_sessions Table +```sql +CREATE TABLE user_sessions ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES fanta_player(id) ON DELETE CASCADE, + session_token VARCHAR(255) UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + last_activity TIMESTAMP DEFAULT NOW(), + expires_at TIMESTAMP NOT NULL, + ip_address INET, + user_agent VARCHAR(500), + is_active BOOLEAN DEFAULT TRUE +); +``` + +## Deployment +The service is deployed using the `@GenezioDeploy()` decorator and can be called from the client application through the Genezio SDK. diff --git a/server/docs/authentication-flow.md b/server/docs/authentication-flow.md new file mode 100644 index 000000000..272168313 --- /dev/null +++ b/server/docs/authentication-flow.md @@ -0,0 +1,309 @@ +# Authentication Flow Documentation + +## Overview + +The Express.js backend uses JWT (JSON Web Tokens) for stateless authentication with session management stored in PostgreSQL. The authentication system includes two middleware functions for protecting routes. + +## Architecture + +``` +Client Request → Express Server → Auth Middleware → Controller → Service → Database + ↓ (if auth fails) + 401/403 Response +``` + +## Middleware Functions + +### 1. `authMiddleware` + +**Purpose**: Validates JWT tokens and ensures the user is authenticated. + +**Location**: `server/src/middleware/auth.middleware.ts` + +**How it works**: +1. Extracts the `Authorization` header from the request +2. Validates the header format (`Bearer `) +3. Verifies the JWT signature using `process.env.JWT_SECRET` +4. Decodes the token to extract user information +5. Attaches user data to `req.user` for downstream use +6. Calls `next()` to proceed to the controller + +**Response codes**: +- `401 Unauthorized`: Token missing, invalid, or expired +- `500 Internal Server Error`: JWT_SECRET not configured or other errors + +**Usage in routes**: +```typescript +import { authMiddleware } from '../middleware/auth.middleware.js'; + +// Protected route - requires valid JWT token +router.post('/profile', authMiddleware, (req, res) => { + // req.user is now available with { userId, username, isAdmin } + controller.getProfile(req, res); +}); +``` + +### 2. `adminMiddleware` + +**Purpose**: Ensures the authenticated user has administrator privileges. + +**Location**: `server/src/middleware/auth.middleware.ts` + +**How it works**: +1. Checks if `req.user` exists (should be set by `authMiddleware`) +2. Verifies that `req.user.isAdmin === true` +3. Returns `403 Forbidden` if user is not an admin +4. Calls `next()` if user is an admin + +**Response codes**: +- `401 Unauthorized`: No user attached to request (missing `authMiddleware`) +- `403 Forbidden`: User is authenticated but not an admin + +**Usage in routes**: +```typescript +import { authMiddleware, adminMiddleware } from '../middleware/auth.middleware.js'; + +// Admin-only route - requires valid JWT + admin flag +router.post('/admin/users', authMiddleware, adminMiddleware, (req, res) => { + // req.user exists and req.user.isAdmin is true + controller.getAllUsers(req, res); +}); +``` + +**IMPORTANT**: `adminMiddleware` must **always** be used together with `authMiddleware` and come **after** it: +```typescript +// ✅ Correct +router.post('/admin-endpoint', authMiddleware, adminMiddleware, controller); + +// ❌ Wrong - adminMiddleware needs req.user from authMiddleware +router.post('/admin-endpoint', adminMiddleware, controller); +``` + +## Request Object Extension + +The middleware extends the Express `Request` type to include user information: + +```typescript +declare global { + namespace Express { + interface Request { + user?: { + userId: number; + username: string; + isAdmin?: boolean; + }; + } + } +} +``` + +Controllers can access this data: +```typescript +export const someController = { + getProfile: (req: Request, res: Response) => { + const userId = req.user!.userId; // Safe to use after authMiddleware + const isAdmin = req.user!.isAdmin; + // ... use userId and isAdmin + } +}; +``` + +## Route Security Matrix + +### Auth Routes (`/api/auth/*`) + +| Endpoint | Method | Auth Required | Admin Required | Description | +|----------|--------|---------------|----------------|-------------| +| `/login` | POST | ❌ No | ❌ No | User login | +| `/register` | POST | ❌ No | ❌ No | User registration | +| `/validate-token` | POST | ❌ No | ❌ No | Token validation | +| `/refresh-token` | POST | ✅ Yes | ❌ No | Refresh JWT token | +| `/logout` | POST | ✅ Yes | ❌ No | Logout current session | +| `/logout-all` | POST | ✅ Yes | ❌ No | Logout all sessions | +| `/sessions` | POST | ✅ Yes | ❌ No | Get user sessions | +| `/change-password` | POST | ✅ Yes | ❌ No | Change own password | +| `/update-user-info` | POST | ✅ Yes | ❌ No | Update own profile | +| `/users` | POST | ✅ Yes | ✅ Yes | Get all users | +| `/admin-change-password` | POST | ✅ Yes | ✅ Yes | Change another user's password | +| `/cleanup-sessions` | POST | ✅ Yes | ✅ Yes | Clean expired sessions | + +### Database Routes (`/api/database/*`) + +| Endpoint | Method | Auth Required | Admin Required | Description | +|----------|--------|---------------|----------------|-------------| +| `/drivers` | POST | ❌ No | ❌ No | Get all drivers | +| `/drivers-data` | POST | ❌ No | ❌ No | Get driver details | +| `/championship` | POST | ❌ No | ❌ No | Get championship standings | +| `/cumulative-points` | POST | ❌ No | ❌ No | Get cumulative points | +| `/tracks` | POST | ❌ No | ❌ No | Get all tracks | +| `/race-results` | POST | ❌ No | ❌ No | Get race results | +| `/seasons` | POST | ❌ No | ❌ No | Get all seasons | +| `/constructors` | POST | ❌ No | ❌ No | Get constructors | +| `/constructor-grand-prix-points` | POST | ❌ No | ❌ No | Get constructor points | +| `/set-gp-result` | POST | ✅ Yes | ✅ Yes | Create/update race results | + +### Fanta Routes (`/api/fanta/*`) + +| Endpoint | Method | Auth Required | Admin Required | Description | +|----------|--------|---------------|----------------|-------------| +| `/votes` | POST | ✅ Yes | ❌ No | Get user's fantasy votes | +| `/set-vote` | POST | ✅ Yes | ❌ No | Submit fantasy vote | + +### Playground Routes (`/api/playground/*`) + +| Endpoint | Method | Auth Required | Admin Required | Description | +|----------|--------|---------------|----------------|-------------| +| `/leaderboard` | POST | ❌ No | ❌ No | View game leaderboard | +| `/score` | POST | ✅ Yes | ❌ No | Submit game score | + +### Twitch Routes (`/api/twitch/*`) + +| Endpoint | Method | Auth Required | Admin Required | Description | +|----------|--------|---------------|----------------|-------------| +| `/stream-info` | POST | ❌ No | ❌ No | Get Twitch stream info | + +## Client-Side Integration + +### Storing the JWT Token + +After successful login, store the JWT token in the Angular app: + +```typescript +// In auth.service.ts +login(username: string, password: string): Observable { + return this.http.post('/api/auth/login', { username, password }) + .pipe( + tap(response => { + if (response.success && response.token) { + // Store token in sessionStorage or localStorage + sessionStorage.setItem('jwt_token', response.token); + } + }) + ); +} +``` + +### Sending Authenticated Requests + +Include the JWT token in the `Authorization` header: + +```typescript +// In api.service.ts or http.interceptor.ts +const token = sessionStorage.getItem('jwt_token'); +const headers = new HttpHeaders({ + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' +}); + +this.http.post('/api/fanta/votes', { raceId: 123 }, { headers }) + .subscribe(response => { + // Handle response + }); +``` + +### HTTP Interceptor (Recommended) + +Create an interceptor to automatically add the token to all requests: + +```typescript +// http.interceptor.ts +export const authInterceptor: HttpInterceptorFn = (req, next) => { + const token = sessionStorage.getItem('jwt_token'); + + if (token) { + const cloned = req.clone({ + setHeaders: { + Authorization: `Bearer ${token}` + } + }); + return next(cloned); + } + + return next(req); +}; +``` + +## Token Structure + +The JWT token contains the following payload: + +```json +{ + "userId": 123, + "username": "john_doe", + "isAdmin": false, + "iat": 1699900000, + "exp": 1699986400 +} +``` + +- `userId`: Database user ID +- `username`: User's username +- `isAdmin`: Admin flag (optional, defaults to false) +- `iat`: Issued at timestamp +- `exp`: Expiration timestamp + +## Error Handling + +### 401 Unauthorized +**Causes**: +- Missing Authorization header +- Invalid token format +- Token expired +- Invalid signature + +**Client action**: Redirect to login page, clear stored token + +### 403 Forbidden +**Causes**: +- Valid token but insufficient privileges (not an admin) + +**Client action**: Show "Access Denied" message, don't clear token + +## Security Best Practices + +1. **Always use HTTPS in production** to prevent token interception +2. **Set appropriate token expiration times** (e.g., 24 hours for regular tokens) +3. **Implement refresh token mechanism** to avoid frequent re-authentication +4. **Never expose JWT_SECRET** - store in environment variables only +5. **Validate tokens on every protected request** - don't trust client-side checks +6. **Implement rate limiting** on auth endpoints to prevent brute force attacks +7. **Log authentication failures** for security monitoring + +## Testing Authentication + +### Test with cURL + +```bash +# Login +curl -X POST http://localhost:3000/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"testuser","password":"testpass"}' + +# Response contains token: +# { "success": true, "token": "eyJhbGc...", "user": {...} } + +# Use token in protected request +curl -X POST http://localhost:3000/api/fanta/votes \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer eyJhbGc..." \ + -d '{"raceId":1,"seasonId":1}' +``` + +### Test with Postman + +1. **Login**: POST to `/api/auth/login` with credentials in body +2. **Copy token** from response +3. **Set Authorization**: In subsequent requests, go to "Authorization" tab → Select "Bearer Token" → Paste token +4. **Send request** to protected endpoints + +--- + +## Summary + +The authentication system provides three levels of access: +1. **Public** - No authentication required (login, register, view data) +2. **Authenticated** - Valid JWT token required (user-specific actions) +3. **Admin** - Valid JWT token + admin flag required (administrative actions) + +The middleware chain ensures security at the route level, preventing unauthorized access before controllers are reached. diff --git a/server/docs/fanta-service.md b/server/docs/fanta-service.md new file mode 100644 index 000000000..30264ecc4 --- /dev/null +++ b/server/docs/fanta-service.md @@ -0,0 +1,328 @@ +# FantaService Documentation + +## Overview + +The `FantaService` class manages fantasy game functionality for the F123 Dashboard application. It handles fantasy player management and fantasy race predictions (votes) where users can predict race outcomes for each Grand Prix. + +## Features + +- Retrieve fantasy votes for all players +- Submit/update fantasy race predictions +- Create new fantasy players +- Input validation for fantasy operations +- Database persistence for fantasy data + +## Database Connection + +The service uses a PostgreSQL connection pool configured with: + +- Connection string from `RACEFORFEDERICA_DB_DATABASE_URL` environment variable +- SSL enabled for secure connections + +## Database Tables Used + +- `fanta_player` - Fantasy player accounts +- `fanta` - Fantasy race predictions/votes + +## Methods + +### Constructor + +```typescript +constructor() +``` + +Initializes the PostgreSQL connection pool with SSL enabled. + +### Public Methods + +#### `getFantaVote(): Promise` + +Retrieves all fantasy votes from all players for all races. + +**Returns:** +- JSON string containing fantasy vote data + +**Response Fields:** +- `fanta_player_id` - Fantasy player identifier +- `track_id` - Race/track identifier +- `username` - Player's username +- `id_1_place` - Predicted 1st place driver ID +- `id_2_place` - Predicted 2nd place driver ID +- `id_3_place` - Predicted 3rd place driver ID +- `id_4_place` - Predicted 4th place driver ID +- `id_5_place` - Predicted 5th place driver ID +- `id_6_place` - Predicted 6th place driver ID +- `id_fast_lap` - Predicted fastest lap driver ID +- `id_dnf` - Predicted DNF (Did Not Finish) driver ID + +**Data Source:** +- Joins `fanta_player` and `fanta` tables +- Ordered by player ID and race ID + +**Example Usage:** +```typescript +const fantaService = new FantaService(); +const votesData = await fantaService.getFantaVote(); +const votes = JSON.parse(votesData); +``` + +#### `setFantaVoto(parameters): Promise` + +Creates or updates a fantasy vote for a specific player and race. + +**Parameters:** +- `fanta_player_id: number` - Fantasy player identifier +- `track_id: number` - Race/track identifier +- `id_1_place: number | null` - Predicted 1st place driver ID +- `id_2_place: number | null` - Predicted 2nd place driver ID +- `id_3_place: number | null` - Predicted 3rd place driver ID +- `id_4_place: number | null` - Predicted 4th place driver ID +- `id_5_place: number | null` - Predicted 5th place driver ID +- `id_6_place: number | null` - Predicted 6th place driver ID +- `id_fast_lap: number | null` - Predicted fastest lap driver ID +- `id_dnf: number | null` - Predicted DNF driver ID + +**Returns:** +- JSON string containing operation result + +**Database Operation:** +- Uses `INSERT ... ON CONFLICT ... DO UPDATE` for upsert functionality +- Conflict resolution based on `(fanta_player_id, race_id)` composite key +- Updates existing vote if combination already exists + +**Validation:** +- Validates required fields (fanta_player_id, track_id) +- Ensures all position predictions are unique (no driver in multiple positions) +- Allows null values for optional predictions + +**Success Response:** +```json +{ + "success": true, + "message": "Fanta vote saved successfully" +} +``` + +**Error Response:** +```json +{ + "success": false, + "message": "Failed to save fanta vote: [error details]" +} +``` + +**Example Usage:** +```typescript +const result = await fantaService.setFantaVoto( + 1, // fanta_player_id + 5, // track_id + 12, // id_1_place + 8, // id_2_place + 3, // id_3_place + 15, // id_4_place + 7, // id_5_place + 2, // id_6_place + 12, // id_fast_lap + 9 // id_dnf +); +``` + +#### `setFantaPlayer(username: string, name: string, surname: string, password: string): Promise` + +Creates a new fantasy player account. + +**Parameters:** +- `username: string` - Unique username (3-50 characters, alphanumeric + underscore) +- `name: string` - Player's first name (1-50 characters) +- `surname: string` - Player's last name (1-50 characters) +- `password: string` - Password (minimum 6 characters) + +**Returns:** +- JSON string containing operation result + +**Validation:** +- All fields are required and must be strings +- Username: 3-50 characters, alphanumeric and underscore only +- Name/Surname: 1-50 characters each +- Password: minimum 6 characters +- Username format validation with regex + +**Success Response:** +```json +{ + "success": true, + "message": "Fanta player created successfully" +} +``` + +**Error Response:** +```json +{ + "success": false, + "message": "Failed to create fanta player: [error details]" +} +``` + +**Example Usage:** +```typescript +const result = await fantaService.setFantaPlayer( + 'john_doe', + 'John', + 'Doe', + 'password123' +); +``` + +### Private Methods + +#### `validateFantaVoto(parameters): void` + +Validates fantasy vote input parameters. + +**Parameters:** +- `fanta_player_id: number` - Fantasy player identifier +- `track_id: number` - Race/track identifier +- `id_1_place: number | null` - through `id_6_place: number | null` - Position predictions + +**Validation Rules:** +- Validates required fields (fanta_player_id, track_id) +- Ensures all position predictions are unique (no duplicates) +- Filters out null/undefined values before uniqueness check + +**Throws:** +- `Error` - If validation fails + +#### `validateFantaPlayer(parameters): void` + +Validates fantasy player creation input. + +**Parameters:** +- `username: string` - Username +- `name: string` - First name +- `surname: string` - Last name +- `password: string` - Password + +**Validation Rules:** +- All fields are required +- All fields must be strings +- Username: 3-50 characters, alphanumeric and underscore only +- Name/Surname: 1-50 characters each +- Password: minimum 6 characters +- Username format validation with regex: `/^[a-zA-Z0-9_]+$/` + +**Throws:** +- `Error` - If validation fails + +## Error Handling + +The service implements comprehensive error handling: + +- All public methods return JSON responses with success/failure status +- Validation errors are caught and returned as structured error messages +- Database errors are logged to console and returned as generic error messages +- Input validation prevents invalid data from reaching the database + +## Database Schema Requirements + +### fanta_player Table +```sql +CREATE TABLE fanta_player ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + name VARCHAR(50) NOT NULL, + surname VARCHAR(50) NOT NULL, + password VARCHAR(255) NOT NULL, + image VARCHAR(255), + created_at TIMESTAMP DEFAULT NOW(), + last_login TIMESTAMP, + password_updated_at TIMESTAMP DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE +); +``` + +### fanta Table +```sql +CREATE TABLE fanta ( + id SERIAL PRIMARY KEY, + fanta_player_id INTEGER NOT NULL REFERENCES fanta_player(id), + race_id INTEGER NOT NULL, + "1_place_id" INTEGER, + "2_place_id" INTEGER, + "3_place_id" INTEGER, + "4_place_id" INTEGER, + "5_place_id" INTEGER, + "6_place_id" INTEGER, + fast_lap_id INTEGER, + dnf_id INTEGER, + season INTEGER, + UNIQUE(fanta_player_id, race_id) +); +``` + +## Usage Example + +```typescript +const fantaService = new FantaService(); + +// Create a new fantasy player +const playerResult = await fantaService.setFantaPlayer( + 'racing_fan', + 'John', + 'Smith', + 'mypassword123' +); + +if (JSON.parse(playerResult).success) { + console.log('Player created successfully'); +} + +// Submit a fantasy vote +const voteResult = await fantaService.setFantaVoto( + 1, // player ID + 3, // race ID + 5, // 1st place prediction + 12, // 2nd place prediction + 8, // 3rd place prediction + 3, // 4th place prediction + 15, // 5th place prediction + 7, // 6th place prediction + 5, // fastest lap prediction + 9 // DNF prediction +); + +// Get all fantasy votes +const allVotes = await fantaService.getFantaVote(); +const votes = JSON.parse(allVotes); +``` + +## Security Considerations + +- Password is stored as plain text (consider implementing hashing) +- Input validation prevents SQL injection through parameterized queries +- Username format validation prevents malicious usernames +- SSL connection for secure database communication + +## Performance Considerations + +- Uses connection pooling for efficient database connections +- Upsert operation for vote updates is efficient +- Consider adding indexes on frequently queried columns +- Fantasy vote retrieval joins two tables - may be slow with large datasets + +## Future Improvements + +1. Implement password hashing for security +2. Add authentication/authorization checks +3. Implement rate limiting for vote submissions +4. Add vote submission deadlines +5. Implement points calculation based on actual race results +6. Add leaderboard functionality +7. Implement user session management +8. Add email validation for players +9. Implement fantasy league functionality +10. Add audit logging for vote changes + +## Deployment + +The service is deployed using the `@GenezioDeploy()` decorator and can be called from the client application through the Genezio SDK. diff --git a/server/docs/postgres-service.md b/server/docs/postgres-service.md new file mode 100644 index 000000000..995c17285 --- /dev/null +++ b/server/docs/postgres-service.md @@ -0,0 +1,267 @@ +# PostgresService Documentation + +## Overview + +The `PostgresService` class provides data access functionality for the F123 Dashboard application. It handles database queries for drivers, championships, tracks, race results, and user management through PostgreSQL database interactions. + +## Features + +- Driver statistics and leaderboard data +- Championship standings and race results +- Track information with best times +- Cumulative points tracking for trend analysis +- Race result management +- User data retrieval +- Season information + +## Database Connection + +The service uses a PostgreSQL connection pool configured with: + +- Connection string from `RACEFORFEDERICA_DB_DATABASE_URL` environment variable +- SSL enabled for secure connections + +## Methods + +### Constructor + +```typescript +constructor() +``` + +Initializes the PostgreSQL connection pool with SSL enabled. + +### Public Methods + +#### `getAllDrivers(): Promise` + +Retrieves comprehensive driver information including statistics and points. + +**Returns:** +- JSON string containing driver data from the `all_race_points` view + +**Response Fields:** +- `driver_id` - Unique driver identifier +- `driver_username` - Driver's username +- `driver_name` - Driver's first name +- `driver_surname` - Driver's last name +- `driver_description` - Driver description +- `driver_license_pt` - License points +- `driver_consistency_pt` - Consistency points +- `driver_fast_lap_pt` - Fast lap points +- `drivers_dangerous_pt` - Dangerous driving points +- `driver_ingenuity_pt` - Ingenuity points +- `driver_strategy_pt` - Strategy points +- `driver_color` - Driver's color/theme +- `pilot_name` - Associated pilot's name +- `pilot_surname` - Associated pilot's surname +- `car_name` - Car name +- `car_overall_score` - Car's overall score +- `total_sprint_points` - Total sprint points +- `total_free_practice_points` - Total free practice points +- `total_qualifying_points` - Total qualifying points +- `total_full_race_points` - Total full race points +- `total_race_points` - Total race points +- `total_points` - Grand total points + +**Example Usage:** +```typescript +const driversData = await postgresService.getAllDrivers(); +const drivers = JSON.parse(driversData); +``` + +#### `getChampionship(): Promise` + +Retrieves comprehensive championship data including all race results across different sessions. + +**Returns:** +- JSON string containing championship data with race results + +**Response Fields:** +- `track_name` - Name of the track +- `gran_prix_date` - Date of the Grand Prix +- `gran_prix_has_sprint` - Boolean indicating if sprint race exists +- `gran_prix_has_x2` - Boolean indicating double points +- `track_country` - Track's country location + +**Race Results (for each session type):** +- `driver_[session]_[position]_place` - Driver username for each position (1-6) +- `driver_[session]_fast_lap` - Driver with fastest lap +- `[session]_dnf` - Did Not Finish drivers (comma-separated) + +**Session Types:** +- `race` - Main race results +- `full_race` - Full race results +- `sprint` - Sprint race results +- `qualifying` - Qualifying results +- `free_practice` - Free practice results + +**Example Usage:** +```typescript +const championshipData = await postgresService.getChampionship(); +const championship = JSON.parse(championshipData); +``` + +#### `getCumulativePoints(): Promise` + +Retrieves cumulative points data for trend analysis and championship progression. + +**Returns:** +- JSON string containing cumulative points data + +**Response Fields:** +- `date` - Grand Prix date +- `track_name` - Track name +- `driver_id` - Driver identifier +- `driver_username` - Driver username +- `driver_color` - Driver's color for visualization +- `cumulative_points` - Running total of points up to this race + +**Usage:** +Perfect for creating championship trend graphs showing how drivers' positions change over the season. + +**Example Usage:** +```typescript +const cumulativeData = await postgresService.getCumulativePoints(); +const trendData = JSON.parse(cumulativeData); +``` + +#### `getAllTracks(): Promise` + +Retrieves information about all tracks in the championship. + +**Returns:** +- JSON string containing track information + +**Response Fields:** +- `track_id` - Unique track identifier +- `name` - Track name +- `date` - Race date +- `has_sprint` - Boolean indicating sprint race +- `has_x2` - Boolean indicating double points +- `country` - Track country +- `besttime_driver_time` - Best lap time on the track +- `username` - Username of driver with best time + +**Ordering:** +Results are ordered by date in ascending order. + +**Example Usage:** +```typescript +const tracksData = await postgresService.getAllTracks(); +const tracks = JSON.parse(tracksData); +``` + +#### `getRaceResoult(): Promise` + +*Note: This method appears to be incomplete in the source code.* + +Retrieves race result information. + +**Returns:** +- JSON string containing race results + +#### `getUsers(): Promise` + +*Note: This method appears to be incomplete in the source code.* + +Retrieves user information. + +**Returns:** +- JSON string containing user data + +#### `getAllSeasons(): Promise` + +*Note: This method appears to be incomplete in the source code.* + +Retrieves season information. + +**Returns:** +- JSON string containing season data + +## Database Views and Tables Used + +### Primary View: `all_race_points` + +This view consolidates driver information with their performance statistics and is used by `getAllDrivers()`. + +### Tables Used by `getChampionship()`: +- `gran_prix` - Grand Prix events +- `tracks` - Track information +- `race_result_entries` - Race results +- `full_race_result_entries` - Full race results +- `sprint_result_entries` - Sprint results +- `qualifying_result_entries` - Qualifying results +- `free_practice_result_entries` - Free practice results +- `drivers` - Driver information + +### Tables Used by `getCumulativePoints()`: +- `driver_grand_prix_points` - Points per Grand Prix per driver +- `drivers` - Driver information for colors + +## Error Handling + +The service does not implement explicit error handling in the provided methods. Database errors will propagate as exceptions. Consider implementing try-catch blocks and returning error responses in JSON format for production use. + +## Performance Considerations + +- Uses connection pooling for efficient database connections +- Complex JOIN operations in `getChampionship()` may be slow with large datasets +- Consider adding indexes on frequently queried columns +- The `all_race_points` view should be optimized for performance + +## Security + +- Uses parameterized queries to prevent SQL injection +- SSL connection enabled for secure database communication +- Environment variable for database connection string + +## Usage Example + +```typescript +const postgresService = new PostgresService(); + +// Get all drivers with their statistics +const driversData = await postgresService.getAllDrivers(); +const drivers = JSON.parse(driversData); + +// Get championship standings +const championshipData = await postgresService.getChampionship(); +const championship = JSON.parse(championshipData); + +// Get cumulative points for trend analysis +const cumulativeData = await postgresService.getCumulativePoints(); +const trendData = JSON.parse(cumulativeData); + +// Get all tracks +const tracksData = await postgresService.getAllTracks(); +const tracks = JSON.parse(tracksData); +``` + +## Database Schema Requirements + +The service expects the following database structure: + +### Core Tables: +- `drivers` - Driver information and statistics +- `tracks` - Track information +- `gran_prix` - Grand Prix events +- `[session]_result_entries` - Results for each session type + +### Views: +- `all_race_points` - Consolidated driver statistics +- `driver_grand_prix_points` - Points per Grand Prix per driver + +## Deployment + +The service is deployed using the `@GenezioDeploy()` decorator and can be called from the client application through the Genezio SDK. + +## Future Improvements + +1. Add comprehensive error handling +2. Implement input validation for future methods +3. Add logging for debugging +4. Optimize complex queries for better performance +5. Complete implementation of incomplete methods +6. Add pagination for large datasets +7. Implement caching for frequently accessed data diff --git a/server/docs/postman/F123Dashboard.postman_collection.json b/server/docs/postman/F123Dashboard.postman_collection.json new file mode 100644 index 000000000..d00878ae6 --- /dev/null +++ b/server/docs/postman/F123Dashboard.postman_collection.json @@ -0,0 +1,1196 @@ +{ + "info": { + "_postman_id": "f123-dashboard-api", + "name": "F123 Dashboard API", + "description": "Complete API collection for F1 Fantasy Dashboard backend services", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt_token}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "// Auto-authentication script", + "// This script automatically logs in if the JWT token is missing or expired", + "", + "const authEndpoints = ['/login', '/register', '/validate', '/health'];", + "const currentPath = pm.request.url.getPath();", + "const isAuthEndpoint = authEndpoints.some(endpoint => currentPath.includes(endpoint));", + "", + "// Skip auto-login for auth endpoints and public endpoints without auth requirement", + "if (isAuthEndpoint) {", + " console.log('Skipping auto-login for auth endpoint');", + " return;", + "}", + "", + "const jwtToken = pm.collectionVariables.get('jwt_token');", + "", + "// Function to perform auto-login", + "function autoLogin() {", + " const username = pm.collectionVariables.get('auto_login_username');", + " const password = pm.collectionVariables.get('auto_login_password');", + " ", + " if (!username || !password) {", + " console.log('Auto-login credentials not set. Please set auto_login_username and auto_login_password variables.');", + " return;", + " }", + " ", + " console.log('Attempting auto-login...');", + " ", + " pm.sendRequest({", + " url: pm.collectionVariables.get('base_url') + '/api/auth/login',", + " method: 'POST',", + " header: {", + " 'Content-Type': 'application/json'", + " },", + " body: {", + " mode: 'raw',", + " raw: JSON.stringify({", + " username: username,", + " password: password", + " })", + " }", + " }, function (err, response) {", + " if (err) {", + " console.error('Auto-login failed:', err);", + " return;", + " }", + " ", + " const jsonResponse = response.json();", + " ", + " if (jsonResponse.success && jsonResponse.token) {", + " pm.collectionVariables.set('jwt_token', jsonResponse.token);", + " pm.collectionVariables.set('user_id', jsonResponse.user.id);", + " pm.collectionVariables.set('is_admin', jsonResponse.user.is_admin);", + " console.log('✅ Auto-login successful! Token saved.');", + " console.log('User ID:', jsonResponse.user.id);", + " console.log('Is Admin:', jsonResponse.user.is_admin);", + " } else {", + " console.error('❌ Auto-login failed:', jsonResponse.message || 'Unknown error');", + " }", + " });", + "}", + "", + "// Check if token exists and is valid", + "if (!jwtToken || jwtToken === '') {", + " console.log('No JWT token found. Attempting auto-login...');", + " autoLogin();", + "} else {", + " // Validate existing token", + " pm.sendRequest({", + " url: pm.collectionVariables.get('base_url') + '/api/auth/validate',", + " method: 'POST',", + " header: {", + " 'Content-Type': 'application/json',", + " 'Authorization': 'Bearer ' + jwtToken", + " },", + " body: {", + " mode: 'raw',", + " raw: '{}'", + " }", + " }, function (err, response) {", + " if (err) {", + " console.error('Token validation error:', err);", + " autoLogin();", + " return;", + " }", + " ", + " const jsonResponse = response.json();", + " ", + " if (!jsonResponse.valid) {", + " console.log('Token expired or invalid. Attempting auto-login...');", + " autoLogin();", + " } else {", + " console.log('✅ Token is valid');", + " }", + " });", + "}", + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// Global test script for JWT token management", + "", + "// Check if response contains a new JWT token and save it", + "if (pm.response.code === 200 || pm.response.code === 201) {", + " try {", + " const jsonResponse = pm.response.json();", + " ", + " // Save JWT token if present in response", + " if (jsonResponse.token) {", + " pm.collectionVariables.set('jwt_token', jsonResponse.token);", + " console.log('🔑 JWT token updated from response');", + " }", + " ", + " // Save user information if present", + " if (jsonResponse.user && jsonResponse.user.id) {", + " pm.collectionVariables.set('user_id', jsonResponse.user.id);", + " pm.collectionVariables.set('is_admin', jsonResponse.user.is_admin);", + " console.log('👤 User info saved:', jsonResponse.user.username);", + " }", + " } catch (e) {", + " // Response is not JSON or doesn't contain expected fields", + " }", + "}", + "", + "// Handle 401 Unauthorized - token might be expired", + "if (pm.response.code === 401) {", + " console.warn('⚠️ 401 Unauthorized - Token may be expired. Run Login request or set auto_login credentials.');", + " pm.collectionVariables.set('jwt_token', '');", + "}", + "" + ] + } + } + ], + "variable": [ + { + "key": "base_url", + "value": "http://localhost:3000", + "type": "string" + }, + { + "key": "jwt_token", + "value": "", + "type": "string" + }, + { + "key": "user_id", + "value": "", + "type": "string" + }, + { + "key": "season_id", + "value": "1", + "type": "string" + }, + { + "key": "is_admin", + "value": "", + "type": "string" + }, + { + "key": "auto_login_username", + "value": "", + "type": "string", + "description": "Set this to enable auto-login (e.g., 'testuser')" + }, + { + "key": "auto_login_password", + "value": "", + "type": "string", + "description": "Set this to enable auto-login (e.g., 'password123')" + } + ], + "item": [ + { + "name": "Health Check", + "item": [ + { + "name": "Health Check", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/health", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "health" + ] + }, + "description": "Check server health and status" + }, + "response": [] + } + ] + }, + { + "name": "Authentication", + "item": [ + { + "name": "Public", + "item": [ + { + "name": "Login", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "// Enhanced login test script", + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('Response has success field', function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('success');", + "});", + "", + "if (pm.response.code === 200) {", + " const response = pm.response.json();", + " ", + " if (response.success && response.token) {", + " // Save authentication data", + " pm.collectionVariables.set('jwt_token', response.token);", + " pm.collectionVariables.set('user_id', response.user.id);", + " pm.collectionVariables.set('is_admin', response.user.is_admin);", + " ", + " // Optional: Save credentials for auto-login", + " const requestBody = JSON.parse(pm.request.body.raw);", + " pm.collectionVariables.set('auto_login_username', requestBody.username);", + " pm.collectionVariables.set('auto_login_password', requestBody.password);", + " ", + " console.log('✅ Login successful!');", + " console.log('🔑 JWT Token:', response.token.substring(0, 20) + '...');", + " console.log('👤 User ID:', response.user.id);", + " console.log('👤 Username:', response.user.username);", + " console.log('👤 Name:', response.user.name, response.user.surname);", + " console.log('🔐 Is Admin:', response.user.is_admin);", + " console.log('💾 Auto-login credentials saved');", + " ", + " pm.test('JWT token received', function () {", + " pm.expect(response.token).to.be.a('string');", + " pm.expect(response.token.length).to.be.above(0);", + " });", + " ", + " pm.test('User object received', function () {", + " pm.expect(response.user).to.be.an('object');", + " pm.expect(response.user.id).to.exist;", + " pm.expect(response.user.username).to.exist;", + " });", + " } else {", + " console.error('❌ Login failed:', response.message);", + " }", + "} else {", + " console.error('❌ Login request failed with status:', pm.response.code);", + "}", + "" + ] + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"testuser\",\n \"password\": \"password123\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/auth/login", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "auth", + "login" + ] + }, + "description": "Login with username and password. Saves JWT token automatically." + }, + "response": [] + }, + { + "name": "Register", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"newuser\",\n \"password\": \"password123\",\n \"name\": \"John\",\n \"surname\": \"Doe\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/auth/register", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "auth", + "register" + ] + }, + "description": "Register a new user account" + }, + "response": [] + }, + { + "name": "Validate Token", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{jwt_token}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{}" + }, + "url": { + "raw": "{{base_url}}/api/auth/validate", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "auth", + "validate" + ] + }, + "description": "Validate JWT token using Authorization header" + }, + "response": [] + } + ] + }, + { + "name": "Protected", + "item": [ + { + "name": "Refresh Token", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{}" + }, + "url": { + "raw": "{{base_url}}/api/auth/refresh-token", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "auth", + "refresh-token" + ] + }, + "description": "Refresh JWT token (requires authentication)" + }, + "response": [] + }, + { + "name": "Logout", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{}" + }, + "url": { + "raw": "{{base_url}}/api/auth/logout", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "auth", + "logout" + ] + }, + "description": "Logout current session" + }, + "response": [] + }, + { + "name": "Logout All Sessions", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{}" + }, + "url": { + "raw": "{{base_url}}/api/auth/logout-all", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "auth", + "logout-all" + ] + }, + "description": "Logout from all active sessions" + }, + "response": [] + }, + { + "name": "Get User Sessions", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{}" + }, + "url": { + "raw": "{{base_url}}/api/auth/sessions", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "auth", + "sessions" + ] + }, + "description": "Get all active sessions for current user" + }, + "response": [] + }, + { + "name": "Change Password", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userId\": {{user_id}},\n \"currentPassword\": \"oldpassword123\",\n \"newPassword\": \"newpassword123\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/auth/change-password", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "auth", + "change-password" + ] + }, + "description": "Change user password" + }, + "response": [] + }, + { + "name": "Update User Info", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userId\": {{user_id}},\n \"name\": \"Updated Name\",\n \"surname\": \"Updated Surname\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/auth/update-user-info", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "auth", + "update-user-info" + ] + }, + "description": "Update user information" + }, + "response": [] + } + ] + }, + { + "name": "Admin Only", + "item": [ + { + "name": "Get All Users", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{}" + }, + "url": { + "raw": "{{base_url}}/api/auth/users", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "auth", + "users" + ] + }, + "description": "Get all users (admin only)" + }, + "response": [] + }, + { + "name": "Admin Change Password", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userId\": 1,\n \"newPassword\": \"newpassword123\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/auth/admin-change-password", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "auth", + "admin-change-password" + ] + }, + "description": "Change any user's password (admin only)" + }, + "response": [] + }, + { + "name": "Cleanup Expired Sessions", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{}" + }, + "url": { + "raw": "{{base_url}}/api/auth/cleanup-sessions", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "auth", + "cleanup-sessions" + ] + }, + "description": "Cleanup expired sessions (admin only)" + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Database", + "item": [ + { + "name": "Drivers", + "item": [ + { + "name": "Get All Drivers", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"seasonId\": {{season_id}}\n}" + }, + "url": { + "raw": "{{base_url}}/api/database/drivers", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "database", + "drivers" + ] + }, + "description": "Get all drivers for a season" + }, + "response": [] + }, + { + "name": "Get Drivers Data", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"seasonId\": {{season_id}}\n}" + }, + "url": { + "raw": "{{base_url}}/api/database/drivers-data", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "database", + "drivers-data" + ] + }, + "description": "Get detailed driver data including stats" + }, + "response": [] + } + ] + }, + { + "name": "Championship", + "item": [ + { + "name": "Get Championship", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"seasonId\": {{season_id}}\n}" + }, + "url": { + "raw": "{{base_url}}/api/database/championship", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "database", + "championship" + ] + }, + "description": "Get championship standings" + }, + "response": [] + }, + { + "name": "Get Cumulative Points", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"seasonId\": {{season_id}}\n}" + }, + "url": { + "raw": "{{base_url}}/api/database/cumulative-points", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "database", + "cumulative-points" + ] + }, + "description": "Get cumulative points over the season" + }, + "response": [] + } + ] + }, + { + "name": "Tracks", + "item": [ + { + "name": "Get All Tracks", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"seasonId\": {{season_id}}\n}" + }, + "url": { + "raw": "{{base_url}}/api/database/tracks", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "database", + "tracks" + ] + }, + "description": "Get all tracks for a season" + }, + "response": [] + } + ] + }, + { + "name": "Race Results", + "item": [ + { + "name": "Get Race Results", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"seasonId\": {{season_id}}\n}" + }, + "url": { + "raw": "{{base_url}}/api/database/race-results", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "database", + "race-results" + ] + }, + "description": "Get race results" + }, + "response": [] + } + ] + }, + { + "name": "Seasons", + "item": [ + { + "name": "Get All Seasons", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{}" + }, + "url": { + "raw": "{{base_url}}/api/database/seasons", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "database", + "seasons" + ] + }, + "description": "Get all available seasons" + }, + "response": [] + } + ] + }, + { + "name": "Constructors", + "item": [ + { + "name": "Get Constructors", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"seasonId\": {{season_id}}\n}" + }, + "url": { + "raw": "{{base_url}}/api/database/constructors", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "database", + "constructors" + ] + }, + "description": "Get constructor teams" + }, + "response": [] + }, + { + "name": "Get Constructor Grand Prix Points", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"seasonId\": {{season_id}}\n}" + }, + "url": { + "raw": "{{base_url}}/api/database/constructor-grand-prix-points", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "database", + "constructor-grand-prix-points" + ] + }, + "description": "Get constructor points per Grand Prix" + }, + "response": [] + } + ] + }, + { + "name": "Admin", + "item": [ + { + "name": "Set GP Result", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"gpId\": 1,\n \"sessionType\": \"race\",\n \"results\": [\n {\n \"position\": 1,\n \"pilotId\": 1,\n \"fastLap\": true\n },\n {\n \"position\": 2,\n \"pilotId\": 2,\n \"fastLap\": false\n }\n ]\n}" + }, + "url": { + "raw": "{{base_url}}/api/database/set-gp-result", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "database", + "set-gp-result" + ] + }, + "description": "Set Grand Prix results (admin only)" + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Fanta (Fantasy)", + "item": [ + { + "name": "Get Fanta Vote", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userId\": {{user_id}},\n \"raceId\": 1,\n \"seasonId\": {{season_id}}\n}" + }, + "url": { + "raw": "{{base_url}}/api/fanta/votes", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "fanta", + "votes" + ] + }, + "description": "Get fantasy votes for a race" + }, + "response": [] + }, + { + "name": "Set Fanta Vote", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userId\": {{user_id}},\n \"raceId\": 1,\n \"seasonId\": {{season_id}},\n \"1_place_id\": 1,\n \"2_place_id\": 2,\n \"3_place_id\": 3,\n \"4_place_id\": 4,\n \"5_place_id\": 5,\n \"6_place_id\": 6,\n \"7_place_id\": 7,\n \"8_place_id\": 8,\n \"fast_lap_id\": 1,\n \"dnf_id\": 9,\n \"team_id\": 1\n}" + }, + "url": { + "raw": "{{base_url}}/api/fanta/set-vote", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "fanta", + "set-vote" + ] + }, + "description": "Submit fantasy vote for a race" + }, + "response": [] + } + ] + }, + { + "name": "Twitch", + "item": [ + { + "name": "Get Stream Info", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"channelName\": \"your_channel_name\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/twitch/stream-info", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "twitch", + "stream-info" + ] + }, + "description": "Get Twitch stream information" + }, + "response": [] + } + ] + }, + { + "name": "Playground", + "item": [ + { + "name": "Get Playground Leaderboard", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{}" + }, + "url": { + "raw": "{{base_url}}/api/playground/leaderboard", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "playground", + "leaderboard" + ] + }, + "description": "Get playground leaderboard" + }, + "response": [] + }, + { + "name": "Set User Best Score", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userId\": {{user_id}},\n \"score\": 1000,\n \"track\": \"Monaco\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/playground/score", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "playground", + "score" + ] + }, + "description": "Submit user's best score (requires authentication)" + }, + "response": [] + } + ] + } + ] +} diff --git a/server/docs/postman/F123Dashboard.postman_environment.json b/server/docs/postman/F123Dashboard.postman_environment.json new file mode 100644 index 000000000..2e5bd1ede --- /dev/null +++ b/server/docs/postman/F123Dashboard.postman_environment.json @@ -0,0 +1,49 @@ +{ + "id": "f123-dashboard-env", + "name": "F123 Dashboard - Local", + "values": [ + { + "key": "base_url", + "value": "http://localhost:3000", + "type": "default", + "enabled": true + }, + { + "key": "jwt_token", + "value": "", + "type": "secret", + "enabled": true + }, + { + "key": "user_id", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "season_id", + "value": "1", + "type": "default", + "enabled": true + }, + { + "key": "is_admin", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "auto_login_username", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "auto_login_password", + "value": "", + "type": "secret", + "enabled": true + } + ], + "_postman_variable_scope": "environment" +} diff --git a/server/docs/postman/F123Dashboard.postman_environment.prod.json b/server/docs/postman/F123Dashboard.postman_environment.prod.json new file mode 100644 index 000000000..94f4a7d28 --- /dev/null +++ b/server/docs/postman/F123Dashboard.postman_environment.prod.json @@ -0,0 +1,49 @@ +{ + "id": "f123-dashboard-env-prod", + "name": "F123 Dashboard - Production", + "values": [ + { + "key": "base_url", + "value": "https://your-production-url.com", + "type": "default", + "enabled": true + }, + { + "key": "jwt_token", + "value": "", + "type": "secret", + "enabled": true + }, + { + "key": "user_id", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "season_id", + "value": "1", + "type": "default", + "enabled": true + }, + { + "key": "is_admin", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "auto_login_username", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "auto_login_password", + "value": "", + "type": "secret", + "enabled": true + } + ], + "_postman_variable_scope": "environment" +} diff --git a/server/docs/postman/POSTMAN_AUTO_AUTH_IMPLEMENTATION.md b/server/docs/postman/POSTMAN_AUTO_AUTH_IMPLEMENTATION.md new file mode 100644 index 000000000..203f4ee04 --- /dev/null +++ b/server/docs/postman/POSTMAN_AUTO_AUTH_IMPLEMENTATION.md @@ -0,0 +1,504 @@ +# Auto-Authentication Implementation Guide + +## Overview + +This Postman collection implements **intelligent auto-authentication** using Postman's event system. It automatically manages JWT tokens, validates them before requests, and re-authenticates when needed. + +## Architecture + +### Event Hooks + +The collection uses two main event hooks: + +1. **Pre-Request Script** (Collection Level) + - Runs **before** every request in the collection + - Handles token validation and auto-login + +2. **Test Script** (Collection Level) + - Runs **after** every response + - Extracts and saves tokens, handles errors + +### Flow Diagram + +``` +┌──────────────────────────────────────────────────────────────┐ +│ REQUEST INITIATED │ +└────────────────────────────┬─────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ PRE-REQUEST SCRIPT EXECUTES │ +├──────────────────────────────────────────────────────────────┤ +│ Check: Is this an auth endpoint? (/login, /register, etc.) │ +│ └─ YES → Skip auto-authentication │ +│ └─ NO → Continue to token check │ +│ │ +│ Check: Do we have a JWT token in variables? │ +│ ├─ NO → Execute Auto-Login │ +│ └─ YES → Validate Token via API │ +│ ├─ VALID → Continue with request │ +│ └─ INVALID → Execute Auto-Login │ +│ │ +│ Auto-Login Process: │ +│ 1. Get credentials from variables │ +│ 2. Send POST to /api/auth/login │ +│ 3. Extract token from response │ +│ 4. Save token to collection variables │ +│ 5. Save user info (id, admin status) │ +└────────────────────────────┬─────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ ACTUAL REQUEST EXECUTES │ +│ (with valid Bearer token in header) │ +└────────────────────────────┬─────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ TEST SCRIPT EXECUTES │ +├──────────────────────────────────────────────────────────────┤ +│ Check response status: │ +│ 200/201 → Success │ +│ └─ Extract JWT token (if present) │ +│ └─ Extract user info (if present) │ +│ └─ Save to collection variables │ +│ │ +│ 401 → Unauthorized │ +│ └─ Clear invalid token │ +│ └─ Log warning message │ +│ │ +│ Other → Process normally │ +└────────────────────────────┬─────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ REQUEST COMPLETE │ +│ (Ready for next request) │ +└──────────────────────────────────────────────────────────────┘ +``` + +## Implementation Details + +### 1. Collection Variables + +The following variables are used for authentication: + +```javascript +{ + jwt_token: "eyJhbGc...", // Current JWT token (auto-managed) + user_id: "1", // Current user ID (auto-managed) + is_admin: "true", // Admin status (auto-managed) + auto_login_username: "testuser", // Login credentials (user-configured) + auto_login_password: "pass123" // Login credentials (user-configured) +} +``` + +### 2. Pre-Request Script + +Located at: **Collection Settings → Pre-request Scripts** + +```javascript +// Skip auth endpoints +const authEndpoints = ['/login', '/register', '/validate-token', '/health']; +const currentPath = pm.request.url.getPath(); +const isAuthEndpoint = authEndpoints.some(endpoint => currentPath.includes(endpoint)); + +if (isAuthEndpoint) { + console.log('Skipping auto-login for auth endpoint'); + return; +} + +const jwtToken = pm.collectionVariables.get('jwt_token'); + +// Auto-login function +function autoLogin() { + const username = pm.collectionVariables.get('auto_login_username'); + const password = pm.collectionVariables.get('auto_login_password'); + + if (!username || !password) { + console.log('Auto-login credentials not set.'); + return; + } + + console.log('Attempting auto-login...'); + + pm.sendRequest({ + url: pm.collectionVariables.get('base_url') + '/api/auth/login', + method: 'POST', + header: { 'Content-Type': 'application/json' }, + body: { + mode: 'raw', + raw: JSON.stringify({ username, password }) + } + }, function (err, response) { + if (err) { + console.error('Auto-login failed:', err); + return; + } + + const jsonResponse = response.json(); + + if (jsonResponse.success && jsonResponse.jwtToken) { + pm.collectionVariables.set('jwt_token', jsonResponse.jwtToken); + pm.collectionVariables.set('user_id', jsonResponse.user.id); + pm.collectionVariables.set('is_admin', jsonResponse.user.is_admin); + console.log('✅ Auto-login successful!'); + } else { + console.error('❌ Auto-login failed:', jsonResponse.message); + } + }); +} + +// Check token validity +if (!jwtToken || jwtToken === '') { + console.log('No JWT token found. Attempting auto-login...'); + autoLogin(); +} else { + // Validate existing token + pm.sendRequest({ + url: pm.collectionVariables.get('base_url') + '/api/auth/validate-token', + method: 'POST', + header: { 'Content-Type': 'application/json' }, + body: { + mode: 'raw', + raw: JSON.stringify({ jwtToken }) + } + }, function (err, response) { + if (err) { + console.error('Token validation error:', err); + autoLogin(); + return; + } + + const jsonResponse = response.json(); + + if (!jsonResponse.valid) { + console.log('Token expired or invalid. Attempting auto-login...'); + autoLogin(); + } else { + console.log('✅ Token is valid'); + } + }); +} +``` + +### 3. Test Script + +Located at: **Collection Settings → Tests** + +```javascript +// Check for successful response +if (pm.response.code === 200 || pm.response.code === 201) { + try { + const jsonResponse = pm.response.json(); + + // Save JWT token if present + if (jsonResponse.jwtToken) { + pm.collectionVariables.set('jwt_token', jsonResponse.jwtToken); + console.log('🔑 JWT token updated from response'); + } + + // Save user information if present + if (jsonResponse.user && jsonResponse.user.id) { + pm.collectionVariables.set('user_id', jsonResponse.user.id); + pm.collectionVariables.set('is_admin', jsonResponse.user.is_admin); + console.log('👤 User info saved:', jsonResponse.user.username); + } + } catch (e) { + // Response not JSON or doesn't contain expected fields + } +} + +// Handle 401 Unauthorized +if (pm.response.code === 401) { + console.warn('⚠️ 401 Unauthorized - Token may be expired.'); + pm.collectionVariables.set('jwt_token', ''); +} +``` + +### 4. Login Request Enhancement + +Located at: **Authentication → Public → Login → Tests** + +```javascript +// Test: Status code is 200 +pm.test('Status code is 200', function () { + pm.response.to.have.status(200); +}); + +// Test: Response has success field +pm.test('Response has success field', function () { + const jsonData = pm.response.json(); + pm.expect(jsonData).to.have.property('success'); +}); + +if (pm.response.code === 200) { + const response = pm.response.json(); + + if (response.success && response.jwtToken) { + // Save authentication data + pm.collectionVariables.set('jwt_token', response.jwtToken); + pm.collectionVariables.set('user_id', response.user.id); + pm.collectionVariables.set('is_admin', response.user.is_admin); + + // Save credentials for auto-login + const requestBody = JSON.parse(pm.request.body.raw); + pm.collectionVariables.set('auto_login_username', requestBody.username); + pm.collectionVariables.set('auto_login_password', requestBody.password); + + console.log('✅ Login successful!'); + console.log('🔑 JWT Token:', response.jwtToken.substring(0, 20) + '...'); + console.log('👤 User ID:', response.user.id); + console.log('👤 Username:', response.user.username); + console.log('🔐 Is Admin:', response.user.is_admin); + console.log('💾 Auto-login credentials saved'); + + // Additional tests + pm.test('JWT token received', function () { + pm.expect(response.jwtToken).to.be.a('string'); + pm.expect(response.jwtToken.length).to.be.above(0); + }); + + pm.test('User object received', function () { + pm.expect(response.user).to.be.an('object'); + pm.expect(response.user.id).to.exist; + pm.expect(response.user.username).to.exist; + }); + } +} +``` + +## Security Considerations + +### Credential Storage + +**Development Environment:** +- Credentials stored in collection/environment variables +- Acceptable for local testing +- Use `.gitignore` to exclude environment files + +**Production Environment:** +- Use Postman Vault for sensitive credentials +- Use environment-specific variables +- Rotate credentials regularly +- Consider using OAuth flows instead + +### Token Exposure + +**Mitigations:** +- Tokens marked as `secret` type in variables +- Tokens not logged in full (only first 20 chars) +- Console output can be disabled in production + +### Network Security + +- Auto-login uses HTTPS in production +- Tokens transmitted in Authorization header (industry standard) +- Server should use short-lived tokens with refresh mechanism + +## Benefits + +### Developer Experience +- ✅ Zero manual token management +- ✅ Seamless testing workflow +- ✅ Works with Collection Runner +- ✅ Works with Newman CLI +- ✅ Automatic token refresh + +### Testing Efficiency +- ✅ Run entire collection without interruption +- ✅ No manual token copying between requests +- ✅ Automatic recovery from expired tokens +- ✅ Consistent authentication across all requests + +### CI/CD Integration +- ✅ Newman supports environment variables +- ✅ Credentials can be injected at runtime +- ✅ Automated API testing possible + +## Usage Examples + +### Example 1: Manual Testing + +```bash +1. Open Postman +2. Import collection +3. Set auto_login_username and auto_login_password +4. Click any request and Send +5. Watch console for auto-authentication logs +``` + +### Example 2: Collection Runner + +```bash +1. Right-click collection → Run +2. Select environment +3. Set iterations (e.g., 10) +4. Run +5. All requests automatically authenticate +``` + +### Example 3: Newman CLI + +```bash +newman run F123Dashboard.postman_collection.json \ + -e F123Dashboard.postman_environment.json \ + --env-var "auto_login_username=testuser" \ + --env-var "auto_login_password=pass123" \ + -r html,cli +``` + +### Example 4: CI/CD Pipeline + +```yaml +# .github/workflows/api-tests.yml +name: API Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install Newman + run: npm install -g newman + + - name: Run API Tests + run: | + newman run server/F123Dashboard.postman_collection.json \ + -e server/F123Dashboard.postman_environment.json \ + --env-var "base_url=${{ secrets.API_URL }}" \ + --env-var "auto_login_username=${{ secrets.TEST_USERNAME }}" \ + --env-var "auto_login_password=${{ secrets.TEST_PASSWORD }}" \ + -r cli,json +``` + +## Troubleshooting + +### Issue: Auto-login not working + +**Symptoms:** Requests fail with 401, no auto-login attempts + +**Solutions:** +1. Check `auto_login_username` and `auto_login_password` are set +2. Verify credentials are correct (test with manual login) +3. Check console for error messages +4. Ensure server is running and accessible + +### Issue: Token validation fails + +**Symptoms:** "Token validation error" in console + +**Solutions:** +1. Verify server is running: `GET /api/health` +2. Check `base_url` is correct +3. Ensure `/api/auth/validate-token` endpoint is working +4. Check network connectivity + +### Issue: Infinite loop of login attempts + +**Symptoms:** Continuous auto-login attempts, no success + +**Solutions:** +1. Check server logs for authentication errors +2. Verify login endpoint returns correct response format +3. Ensure `jwtToken` field exists in login response +4. Check password is correct + +### Issue: Scripts not executing + +**Symptoms:** No console output, manual token management needed + +**Solutions:** +1. Check scripts are enabled: Settings → General → Allow scripts +2. Verify collection-level scripts exist +3. Re-import collection if scripts missing +4. Check Postman Console for script errors + +## Customization + +### Custom Authentication Endpoint + +If your API uses different authentication endpoints: + +```javascript +// In pre-request script, change: +url: pm.collectionVariables.get('base_url') + '/api/auth/login' + +// To: +url: pm.collectionVariables.get('base_url') + '/your/custom/auth' +``` + +### Custom Token Field Name + +If your API returns tokens with a different field name: + +```javascript +// In test script, change: +if (jsonResponse.jwtToken) { + +// To: +if (jsonResponse.accessToken) { // or your field name +``` + +### Add Refresh Token Support + +```javascript +// In pre-request script, add: +function refreshToken() { + const refreshToken = pm.collectionVariables.get('refresh_token'); + + pm.sendRequest({ + url: pm.collectionVariables.get('base_url') + '/api/auth/refresh', + method: 'POST', + body: { + mode: 'raw', + raw: JSON.stringify({ refreshToken }) + } + }, function (err, response) { + // Handle refresh token response + }); +} +``` + +## Best Practices + +1. **Separate Environments**: Use different environments for dev/staging/prod +2. **Secure Credentials**: Never commit credentials to version control +3. **Token Expiration**: Set reasonable token expiration times server-side +4. **Error Handling**: Monitor Postman Console for authentication errors +5. **Testing**: Test auto-authentication with expired tokens +6. **Documentation**: Keep this guide updated with customizations + +## Maintenance + +### Updating Scripts + +1. Export collection from Postman +2. Edit JSON file (or use Postman UI) +3. Re-import to apply changes +4. Test thoroughly + +### Version Control + +```bash +# Recommended .gitignore entries +*.postman_environment.json # Contains credentials +*private*.json # Private test data +``` + +### Monitoring + +Use Postman Monitors to: +- Run tests on schedule +- Alert on failures +- Track API performance +- Validate authentication flow + +--- + +**Version:** 1.0.0 +**Author:** F123 Dashboard Team +**Last Updated:** November 2025 diff --git a/server/docs/postman/POSTMAN_FILE_INDEX.md b/server/docs/postman/POSTMAN_FILE_INDEX.md new file mode 100644 index 000000000..084927292 --- /dev/null +++ b/server/docs/postman/POSTMAN_FILE_INDEX.md @@ -0,0 +1,265 @@ +# Postman Collection - File Index + +This directory contains a complete Postman testing suite for the F123 Dashboard API with intelligent auto-authentication. + +## 📦 Files Overview + +### Core Collection Files + +| File | Purpose | Import Required | +|------|---------|-----------------| +| `F123Dashboard.postman_collection.json` | Main API collection with 40+ endpoints | ✅ Yes | +| `F123Dashboard.postman_environment.json` | Local development environment | ✅ Yes | +| `F123Dashboard.postman_environment.prod.json` | Production environment template | ✅ Yes | + +### Documentation Files + +| File | Purpose | For | +|------|---------|-----| +| `POSTMAN_README.md` | Comprehensive guide with all features | Everyone | +| `POSTMAN_QUICK_GUIDE.md` | Visual quick reference | Quick start | +| `POSTMAN_AUTO_AUTH_IMPLEMENTATION.md` | Technical implementation details | Developers | +| `POSTMAN_FILE_INDEX.md` | This file - navigation guide | Reference | + +## 🚀 Getting Started + +### First Time Users + +1. **Read:** `POSTMAN_QUICK_GUIDE.md` (5 min) +2. **Import:** All 3 JSON files into Postman +3. **Configure:** Set auto_login credentials +4. **Test:** Run any request + +### Need Detailed Info? + +- **Full Documentation:** `POSTMAN_README.md` +- **Technical Details:** `POSTMAN_AUTO_AUTH_IMPLEMENTATION.md` + +## 📚 Document Guide + +### POSTMAN_README.md +**Length:** ~450 lines +**Reading Time:** 15-20 minutes + +**Contents:** +- Quick start instructions +- File descriptions +- Collection structure (all endpoints) +- Auto-authentication overview +- Usage guide with examples +- Environment variables reference +- Request examples +- Tips & best practices +- Troubleshooting +- Newman CLI usage +- API endpoints table + +**Best For:** +- Complete reference +- First-time setup +- Troubleshooting +- API endpoint lookup + +### POSTMAN_QUICK_GUIDE.md +**Length:** ~350 lines +**Reading Time:** 10-15 minutes + +**Contents:** +- Visual flow diagrams +- 3-step setup guide +- Collection structure tree +- Variables reference table +- Testing workflows +- Troubleshooting quick fixes +- Response examples +- Power user tips + +**Best For:** +- Visual learners +- Quick reference +- Workflow planning +- Console output examples + +### POSTMAN_AUTO_AUTH_IMPLEMENTATION.md +**Length:** ~500 lines +**Reading Time:** 20-25 minutes + +**Contents:** +- Architecture overview +- Detailed flow diagrams +- Complete script implementations +- Security considerations +- Benefits analysis +- Usage examples with code +- Troubleshooting deep-dive +- Customization guide +- CI/CD integration examples + +**Best For:** +- Developers +- Customization needs +- Understanding internals +- CI/CD integration +- Security review + +## 🎯 Use Case Matrix + +| I want to... | Read this file | +|-------------|----------------| +| Get started quickly | `POSTMAN_QUICK_GUIDE.md` | +| Understand all features | `POSTMAN_README.md` | +| Customize authentication | `POSTMAN_AUTO_AUTH_IMPLEMENTATION.md` | +| Find an endpoint | `POSTMAN_README.md` (API table) | +| See visual flow | `POSTMAN_QUICK_GUIDE.md` | +| Integrate with CI/CD | `POSTMAN_AUTO_AUTH_IMPLEMENTATION.md` | +| Troubleshoot issues | All three (different perspectives) | +| Learn about security | `POSTMAN_AUTO_AUTH_IMPLEMENTATION.md` | + +## 🔑 Key Features Across All Docs + +### Auto-Authentication +- ✅ Automatic login when token missing +- ✅ Token validation before requests +- ✅ Seamless token refresh +- ✅ Works with Collection Runner +- ✅ Newman CLI compatible + +### Collection Coverage +- 40+ API endpoints +- 5 main service areas +- Public, protected, and admin endpoints +- Comprehensive request examples +- Built-in test scripts + +### Documentation Quality +- Step-by-step guides +- Visual diagrams +- Code examples +- Troubleshooting sections +- Best practices + +## 📖 Reading Recommendations + +### For New Users +1. Start with `POSTMAN_QUICK_GUIDE.md` +2. Import files and test basic requests +3. Reference `POSTMAN_README.md` as needed + +### For Developers +1. Skim `POSTMAN_QUICK_GUIDE.md` for overview +2. Deep dive into `POSTMAN_AUTO_AUTH_IMPLEMENTATION.md` +3. Keep `POSTMAN_README.md` open for API reference + +### For Team Leads +1. Review all three documents +2. Understand security implications +3. Plan CI/CD integration strategy +4. Customize for team needs + +## 🛠️ Maintenance + +### Updating Collection +1. Make changes in Postman +2. Export updated JSON files +3. Update relevant documentation +4. Test all workflows +5. Update version numbers + +### Adding Endpoints +1. Add to Postman collection +2. Update endpoint table in `POSTMAN_README.md` +3. Add to structure tree in `POSTMAN_QUICK_GUIDE.md` +4. Add examples if complex + +### Customizing Scripts +1. Modify in Postman or JSON +2. Document changes in `POSTMAN_AUTO_AUTH_IMPLEMENTATION.md` +3. Update troubleshooting if needed +4. Test thoroughly + +## 🔄 Version History + +| Version | Date | Changes | +|---------|------|---------| +| 1.0.0 | 2025-11-14 | Initial release with auto-authentication | + +## 📞 Support + +### For Questions +- Check troubleshooting sections in all docs +- Review console output in Postman +- Check server logs for authentication errors + +### For Issues +- Verify server is running: `GET /api/health` +- Check credentials are set correctly +- Review Postman Console for errors +- Ensure scripts are enabled in Postman + +### For Enhancements +- Document feature requests +- Test thoroughly before deployment +- Update all relevant documentation +- Share with team + +## 🎓 Learning Path + +``` +Start Here + ↓ +POSTMAN_QUICK_GUIDE.md + ↓ +Try importing & testing + ↓ +Need more details? + ↓ +POSTMAN_README.md + ↓ +Want to customize? + ↓ +POSTMAN_AUTO_AUTH_IMPLEMENTATION.md + ↓ +Become a power user! 🚀 +``` + +## 📊 Documentation Statistics + +| Metric | Value | +|--------|-------| +| Total Documentation Lines | ~1,300 | +| API Endpoints Documented | 40+ | +| Code Examples | 20+ | +| Troubleshooting Scenarios | 15+ | +| Visual Diagrams | 5 | +| Environment Variables | 7 | +| Security Considerations | Comprehensive | + +## 🏆 Best Practices Summary + +1. **Always use environments** - Separate dev/prod +2. **Enable auto-authentication** - Save time +3. **Monitor console** - Understand what's happening +4. **Secure credentials** - Never commit to git +5. **Test thoroughly** - Use Collection Runner +6. **Document changes** - Keep docs updated +7. **Review security** - Understand implications + +## 🔗 Related Resources + +- **Server Documentation:** `../docs/` +- **API Implementation:** `../src/controllers/` +- **Authentication Flow:** `../docs/authentication-flow.md` +- **Database Schema:** `../../.github/instructions/db.instructions.md` + +### 🎯 Success Indicators: +- Console shows "✅ Token is valid" +- Requests succeed without manual token entry +- Collection Runner completes without errors +- Newman CLI runs successfully + +--- + +**Collection Version:** 1.0.0 +**Documentation Version:** 1.0.0 +**Last Updated:** November 14, 2025 +**Maintained By:** F123 Dashboard Team diff --git a/server/docs/postman/POSTMAN_FLOW_DIAGRAMS.md b/server/docs/postman/POSTMAN_FLOW_DIAGRAMS.md new file mode 100644 index 000000000..0d999a074 --- /dev/null +++ b/server/docs/postman/POSTMAN_FLOW_DIAGRAMS.md @@ -0,0 +1,414 @@ +# Auto-Authentication Visual Flow + +## 🎬 Complete Request Lifecycle + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ USER ACTION: Click "Send" on any request │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 🔍 PRE-REQUEST SCRIPT BEGINS │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ Step 1: Check Request Type │ +│ ┌───────────────────────────────────────────────────┐ │ +│ │ Is this /login, /register, /validate-token, or │ │ +│ │ /health endpoint? │ │ +│ └──────────┬───────────────────────────────┬────────┘ │ +│ │ YES │ NO │ +│ ▼ ▼ │ +│ ┌──────────────┐ ┌────────────────┐ │ +│ │ Skip Auth │ │ Check Token │ │ +│ │ Continue → │ │ Exists? │ │ +│ └──────────────┘ └────────┬───────┘ │ +│ │ │ +│ ┌────────────────────────┴─────┐ │ +│ │ NO │ YES │ +│ ▼ ▼ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ AUTO-LOGIN │ │ VALIDATE TOKEN │ │ +│ │ Flow A │ │ Flow B │ │ +│ └────────┬─────────┘ └────────┬─────────┘ │ +│ │ │ │ +│ └──────────────┬───────────────┘ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ Token Ready! │ │ +│ │ Continue Request │ │ +│ └──────────────────┘ │ +│ │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 🚀 ACTUAL REQUEST EXECUTES │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ Request Headers: │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Authorization: Bearer {{jwt_token}} │ │ +│ │ Content-Type: application/json │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ +│ Request sent to: {{base_url}}/api/endpoint │ +│ │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 📥 RESPONSE RECEIVED │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 🧪 TEST SCRIPT EXECUTES │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ Check Response Status Code │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ What status did we get? │ │ +│ └──┬──────────────┬──────────────┬────────────────┘ │ +│ │ 200/201 │ 401 │ Other │ +│ ▼ ▼ ▼ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ SUCCESS │ │ UNAUTH │ │ OTHER │ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ Extract Clear Process │ +│ Token & Token Normally │ +│ User Info │ +│ │ │ +│ ▼ │ +│ Save to Variables: │ +│ ┌──────────────────────────────────────┐ │ +│ │ jwt_token → "eyJhbGc..." │ │ +│ │ user_id → 1 │ │ +│ │ is_admin → true │ │ +│ └──────────────────────────────────────┘ │ +│ │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ ✅ REQUEST COMPLETE │ +│ Variables updated, ready for next request │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## 🔄 Flow A: Auto-Login Process (No Token) + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ No JWT Token Found │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Get Credentials from Variables │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ auto_login_username → "testuser" │ │ +│ │ auto_login_password → "password123" │ │ +│ └──────────────────────────────────────────────────┘ │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ + ┌────────────────────┐ + │ Credentials Set? │ + └────────┬───────────┘ + │ + ┌─────────────┴──────────────┐ + │ NO │ YES + ▼ ▼ + ┌──────────────────┐ ┌──────────────────────┐ + │ Log Warning │ │ Send Login Request │ + │ Skip Auto-Login │ │ POST /api/auth/login │ + └──────────────────┘ └──────────┬───────────┘ + │ + ▼ + ┌──────────────────┐ + │ Login Response │ + └────────┬─────────┘ + │ + ┌─────────────────┴─────────────────┐ + │ Success? │ + └─────┬─────────────────────────┬────┘ + │ YES │ NO + ▼ ▼ + ┌──────────────────┐ ┌──────────────────┐ + │ Extract Token │ │ Log Error │ + │ & User Info │ │ Continue without │ + └────────┬─────────┘ └──────────────────┘ + │ + ▼ + ┌──────────────────────────────┐ + │ Save to Collection Variables │ + │ - jwt_token │ + │ - user_id │ + │ - is_admin │ + └──────────────────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ ✅ Token Ready! │ + │ Continue Request │ + └──────────────────┘ +``` + +## 🔍 Flow B: Token Validation Process (Has Token) + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ JWT Token Exists in Variables │ +│ Token: "eyJhbGciOiJIUzI1NiIsInR5c..." │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Send Validation Request │ +│ POST /api/auth/validate-token │ +│ Body: { "jwtToken": "{{jwt_token}}" } │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ Validation │ + │ Response │ + └────────┬─────────┘ + │ + ┌─────────────┴──────────────┐ + │ │ + Valid: true Valid: false + │ │ + ▼ ▼ + ┌──────────────────┐ ┌──────────────────────┐ + │ ✅ Token Valid │ │ ❌ Token Expired │ + │ Use Existing │ │ Trigger Auto-Login │ + │ Token │ │ (Go to Flow A) │ + └────────┬─────────┘ └──────────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ Continue with │ + │ Current Request │ + └──────────────────┘ +``` + +## 📊 Variable State Lifecycle + +``` +INITIAL STATE (No Auth) +┌─────────────────────────────────────┐ +│ jwt_token: "" │ +│ user_id: "" │ +│ is_admin: "" │ +│ auto_login_username: "testuser" │ +│ auto_login_password: "pass123" │ +└─────────────────────────────────────┘ + │ + │ (First Request Triggers Auto-Login) + ▼ +AUTHENTICATED STATE +┌─────────────────────────────────────┐ +│ jwt_token: "eyJhbGc..." │ +│ user_id: "1" │ +│ is_admin: "true" │ +│ auto_login_username: "testuser" │ +│ auto_login_password: "pass123" │ +└─────────────────────────────────────┘ + │ + │ (Token Expires / 401 Error) + ▼ +EXPIRED STATE +┌─────────────────────────────────────┐ +│ jwt_token: "" ← Cleared │ +│ user_id: "1" │ +│ is_admin: "true" │ +│ auto_login_username: "testuser" │ +│ auto_login_password: "pass123" │ +└─────────────────────────────────────┘ + │ + │ (Next Request Triggers Auto-Login) + ▼ +AUTHENTICATED STATE (Again) +┌─────────────────────────────────────┐ +│ jwt_token: "eyJNewToken..." │ +│ user_id: "1" │ +│ is_admin: "true" │ +│ auto_login_username: "testuser" │ +│ auto_login_password: "pass123" │ +└─────────────────────────────────────┘ +``` + +## 🎭 Request Type Decision Tree + +``` + Request Initiated + │ + ▼ + ┌────────────────────────┐ + │ Check Request Path │ + └────────────┬───────────┘ + │ + ┏━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━┓ + ▼ ▼ +┌───────────────┐ ┌────────────────┐ +│ Auth Endpoint │ │ Regular │ +│ /login │ │ Endpoint │ +│ /register │ │ /api/database │ +│ /validate │ │ /api/fanta │ +│ /health │ │ etc. │ +└───────┬───────┘ └────────┬───────┘ + │ │ + ▼ ▼ +┌───────────────┐ ┌────────────────┐ +│ Skip │ │ Check/Get │ +│ Auto-Auth │ │ Token │ +└───────┬───────┘ └────────┬───────┘ + │ │ + └──────────────┬──────────────────────┘ + │ + ▼ + Execute Request with + Appropriate Auth State +``` + +## 🔐 Authentication State Machine + +``` + ┌─────────────────┐ + │ NO TOKEN │ + │ (Initial) │ + └────────┬────────┘ + │ + │ Auto-Login Success + ▼ + ┌─────────────────┐ + │ TOKEN VALID │◄───────┐ + │ (Authenticated)│ │ + └────────┬────────┘ │ + │ │ + │ Token Expires │ Refresh/ + │ or 401 Error │ Re-login + ▼ │ + ┌─────────────────┐ │ + │ TOKEN EXPIRED │ │ + │ (Needs Refresh)│────────┘ + └─────────────────┘ + │ + │ Logout / Clear + ▼ + ┌─────────────────┐ + │ NO TOKEN │ + │ (Logged Out) │ + └─────────────────┘ +``` + +## 📈 Typical Usage Flow + +``` +Day 1: First Time Setup +┌────────────────────────────────────┐ +│ 1. Import collection │ +│ 2. Set auto_login credentials │ +│ 3. Run any request │ +│ └─→ Auto-login happens ✅ │ +│ 4. All subsequent requests work │ +└────────────────────────────────────┘ + +Day 2: Resume Work (Token Expired) +┌────────────────────────────────────┐ +│ 1. Open Postman │ +│ 2. Run any request │ +│ └─→ Token validation fails │ +│ └─→ Auto-login happens ✅ │ +│ 3. Continue testing │ +└────────────────────────────────────┘ + +Day 3: Testing Multiple Users +┌────────────────────────────────────┐ +│ 1. Change auto_login_username │ +│ 2. Clear jwt_token │ +│ 3. Run request │ +│ └─→ Auto-login with new user ✅ │ +│ 4. Test with different user │ +└────────────────────────────────────┘ +``` + +## 🎬 Collection Runner Flow + +``` +Collection Runner Started + │ + ▼ +┌───────────────────────────┐ +│ Request 1: Login │ ← Manual login +│ - Saves token ✅ │ +└──────────┬────────────────┘ + │ + ▼ +┌───────────────────────────┐ +│ Request 2: Get Drivers │ ← Uses saved token +│ - Token valid ✅ │ +└──────────┬────────────────┘ + │ + ▼ +┌───────────────────────────┐ +│ Request 3: Get Seasons │ ← Uses saved token +│ - Token valid ✅ │ +└──────────┬────────────────┘ + │ + ▼ +┌───────────────────────────┐ +│ Request 4: Submit Vote │ ← Uses saved token +│ - Token valid ✅ │ +└──────────┬────────────────┘ + │ + ▼ + [Continue...] + +All 40+ Requests Complete ✅ +``` + +## 🔧 Newman CLI Flow + +```bash +$ newman run collection.json -e environment.json + +Newman Run Starting... + │ + ▼ +Pre-Request Scripts Initialize + │ + ▼ +First Request: Health Check (No Auth) +✓ Response 200 OK + │ + ▼ +Second Request: Get Drivers +→ Pre-Request: No token found +→ Auto-login triggered +→ Login successful ✅ +→ Token saved +✓ Response 200 OK + │ + ▼ +Third Request: Get Championship +→ Pre-Request: Token found +→ Token validated +→ Token valid ✅ +✓ Response 200 OK + │ + ▼ +[All remaining requests use valid token] + │ + ▼ +Newman Run Complete ✅ +Summary: 40/40 passed +``` + +--- + +**Tip:** Open Postman Console (Ctrl+Alt+C / Cmd+Alt+C) to see these flows in real-time as you test! diff --git a/server/docs/postman/POSTMAN_QUICK_GUIDE.md b/server/docs/postman/POSTMAN_QUICK_GUIDE.md new file mode 100644 index 000000000..39b198544 --- /dev/null +++ b/server/docs/postman/POSTMAN_QUICK_GUIDE.md @@ -0,0 +1,364 @@ +# Postman Collection - Quick Reference Guide + +## 🚀 Setup in 3 Steps + +### Step 1: Import Files +``` +Postman → Import → Select Files: + ✅ F123Dashboard.postman_collection.json + ✅ F123Dashboard.postman_environment.json + ✅ F123Dashboard.postman_environment.prod.json +``` + +### Step 2: Select Environment +``` +Top-right dropdown → "F123 Dashboard - Local" +``` + +### Step 3: Enable Auto-Login +``` +Eye icon (👁️) → Edit → Set: + - auto_login_username: "your_username" + - auto_login_password: "your_password" +``` + +## 🤖 Auto-Authentication + +### How It Works + +``` +┌─────────────────────────────────────────────────────────┐ +│ You: Run ANY Request │ +└────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Pre-Request Script (Automatic) │ +├─────────────────────────────────────────────────────────┤ +│ 1. Do we have a JWT token? │ +│ ├─ NO → Auto-login with saved credentials │ +│ └─ YES → Validate token │ +│ ├─ VALID → Continue │ +│ └─ EXPIRED → Auto-login │ +└────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Request Executes with Valid Token │ +└────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Test Script (Automatic) │ +├─────────────────────────────────────────────────────────┤ +│ 1. Extract JWT token from response (if present) │ +│ 2. Save user info (ID, admin status) │ +│ 3. Handle errors (401 → clear token) │ +└────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Done! Ready for next request │ +└─────────────────────────────────────────────────────────┘ +``` + +### What You See in Console + +#### ✅ First Time (Auto-Login) +``` +No JWT token found. Attempting auto-login... +✅ Auto-login successful! Token saved. +User ID: 1 +Is Admin: true +``` + +#### ✅ Subsequent Requests (Token Valid) +``` +✅ Token is valid +``` + +#### ⚠️ Token Expired +``` +Token expired or invalid. Attempting auto-login... +✅ Auto-login successful! Token saved. +``` + +## 📁 Collection Structure + +``` +F123 Dashboard API +│ +├── 🏥 Health Check +│ └── Health Check (GET, no auth) +│ +├── 🔐 Authentication +│ ├── Public (no auth required) +│ │ ├── Login ⭐ (saves token automatically) +│ │ ├── Register +│ │ └── Validate Token +│ │ +│ ├── Protected (requires token) +│ │ ├── Refresh Token +│ │ ├── Logout +│ │ ├── Logout All Sessions +│ │ ├── Get User Sessions +│ │ ├── Change Password +│ │ └── Update User Info +│ │ +│ └── Admin Only (requires admin) +│ ├── Get All Users +│ ├── Admin Change Password +│ └── Cleanup Expired Sessions +│ +├── 💾 Database (public endpoints) +│ ├── Drivers +│ │ ├── Get All Drivers +│ │ └── Get Drivers Data +│ │ +│ ├── Championship +│ │ ├── Get Championship +│ │ └── Get Cumulative Points +│ │ +│ ├── Tracks +│ │ └── Get All Tracks +│ │ +│ ├── Race Results +│ │ └── Get Race Results +│ │ +│ ├── Seasons +│ │ └── Get All Seasons +│ │ +│ ├── Constructors +│ │ ├── Get Constructors +│ │ └── Get Constructor Grand Prix Points +│ │ +│ └── Admin +│ └── Set GP Result (admin only) +│ +├── 🏎️ Fanta (Fantasy) (requires auth) +│ ├── Get Fanta Vote +│ └── Set Fanta Vote +│ +├── 📺 Twitch (public) +│ └── Get Stream Info +│ +└── 🎮 Playground + ├── Get Playground Leaderboard (public) + └── Set User Best Score (requires auth) +``` + +## 🔑 Variables Reference + +### Collection Variables (auto-managed) + +| Variable | Type | Description | Auto-Populated | +|----------|------|-------------|----------------| +| `base_url` | string | API base URL | ❌ Manual | +| `jwt_token` | secret | JWT authentication token | ✅ Yes | +| `user_id` | string | Current user ID | ✅ Yes | +| `season_id` | string | Default season ID | ❌ Manual | +| `is_admin` | boolean | Admin flag | ✅ Yes | +| `auto_login_username` | string | Auto-login username | ⚙️ Optional | +| `auto_login_password` | secret | Auto-login password | ⚙️ Optional | + +### Usage in Requests + +``` +URL: {{base_url}}/api/auth/login +Body: "userId": {{user_id}} +Auth: Bearer {{jwt_token}} +``` + +## 🧪 Testing Workflows + +### Workflow 1: First Time User Registration + +``` +1. Register (Public) + POST /api/auth/register + Body: username, password, name, surname + +2. Login (Public) + POST /api/auth/login + Body: username, password + Result: Token saved automatically ✅ + +3. Any Protected Endpoint + All subsequent requests use saved token ✅ +``` + +### Workflow 2: Existing User with Auto-Login + +``` +1. Set auto_login credentials once (eye icon → edit) + +2. Run ANY request + - Token missing? → Auto-login ✅ + - Token expired? → Auto-login ✅ + - Token valid? → Use it ✅ +``` + +### Workflow 3: Admin Testing + +``` +1. Login with admin account + +2. Test admin endpoints + - Get All Users + - Admin Change Password + - Cleanup Sessions + - Set GP Result + +All use same authentication ✅ +``` + +### Workflow 4: Fantasy Game Testing + +``` +1. Login (get token) + +2. Get Drivers (see available drivers) + POST /api/database/drivers + +3. Submit Fantasy Vote + POST /api/fanta/set-vote + Body: positions 1-8, fast_lap_id, dnf_id, team_id + +4. Get Vote Back + POST /api/fanta/votes + Verify your submission ✅ +``` + +## 🐛 Troubleshooting + +### ❌ "Auto-login credentials not set" +**Solution:** Set `auto_login_username` and `auto_login_password` in collection variables + +### ❌ "401 Unauthorized" +**Possible Causes:** +- Token expired → Auto-login should handle this +- Auto-login failed → Check credentials +- Invalid endpoint → Check URL + +**Manual Fix:** +1. Clear `jwt_token` variable +2. Run Login request manually +3. Check console for errors + +### ❌ "403 Forbidden" +**Cause:** Endpoint requires admin privileges + +**Solution:** Login with admin account + +### ❌ "Token validation error" +**Cause:** Server not running or network error + +**Solution:** +1. Verify server is running: `GET /api/health` +2. Check `base_url` is correct +3. Check network connection + +### ❌ Requests failing silently +**Check Console:** +1. Postman → View → Show Postman Console (Ctrl+Alt+C) +2. Look for auto-authentication logs +3. Check for error messages + +## 🎯 Best Practices + +### ✅ DO +- Use auto-login for development testing +- Keep credentials in environment variables +- Use Collection Runner for batch testing +- Check Postman Console for debug info +- Use different environments for dev/prod + +### ❌ DON'T +- Commit credentials to version control +- Share collections with hardcoded passwords +- Use production credentials in local environment +- Disable auto-authentication scripts (unless needed) + +## 🔥 Power User Tips + +### Tip 1: Batch Testing with Collection Runner +``` +1. Click "..." on collection → Run collection +2. Select environment +3. Set iterations +4. All requests auto-authenticate ✅ +``` + +### Tip 2: Newman CLI Integration +```bash +# Install Newman +npm install -g newman + +# Run collection +newman run F123Dashboard.postman_collection.json \ + -e F123Dashboard.postman_environment.json \ + --env-var "auto_login_username=testuser" \ + --env-var "auto_login_password=pass123" +``` + +### Tip 3: Custom Test Scripts +Add to individual requests: +```javascript +pm.test("Response time < 200ms", function () { + pm.expect(pm.response.responseTime).to.be.below(200); +}); + +pm.test("Specific data validation", function () { + const data = pm.response.json(); + pm.expect(data.drivers).to.be.an('array'); + pm.expect(data.drivers.length).to.be.above(0); +}); +``` + +### Tip 4: Environment Switching +Switch between local/prod instantly: +- Local: `http://localhost:3000` +- Production: `https://api.yoursite.com` + +Same collection works for both! 🎉 + +## 📊 Response Examples + +### Success Response (Login) +```json +{ + "success": true, + "message": "Login successful", + "jwtToken": "eyJhbGc...", + "user": { + "id": 1, + "username": "testuser", + "name": "John", + "surname": "Doe", + "is_admin": false + } +} +``` + +### Error Response (401) +```json +{ + "success": false, + "message": "Invalid credentials" +} +``` + +### Error Response (403) +```json +{ + "success": false, + "message": "Admin access required" +} +``` + +--- + +**Need Help?** Check the full POSTMAN_README.md for detailed documentation. + +**Version:** 1.0.0 +**Last Updated:** November 2025 diff --git a/server/docs/postman/POSTMAN_README.md b/server/docs/postman/POSTMAN_README.md new file mode 100644 index 000000000..c351ad5f2 --- /dev/null +++ b/server/docs/postman/POSTMAN_README.md @@ -0,0 +1,486 @@ +# F123 Dashboard API - Postman Collection + +🚀 **Smart Auto-Authentication Enabled** - No manual token management required! + +This directory contains Postman collections and environments for testing the F123 Dashboard backend API with intelligent auto-authentication. + +## ⚡ Quick Start (TL;DR) + +1. Import all 3 JSON files into Postman +2. Select "F123 Dashboard - Local" environment +3. Edit collection variables: Set `auto_login_username` and `auto_login_password` +4. Run any request - authentication happens automatically! + +## ✨ Key Features + +- 🤖 **Auto-Authentication** - Automatically logs in when token is missing/expired +- 🔄 **Token Validation** - Validates tokens before each request +- 💾 **Auto-Save** - Saves tokens and user info automatically +- 🧪 **Test Scripts** - Built-in tests for all endpoints +- 🌍 **Multiple Environments** - Local and production configurations +- 📝 **Comprehensive Docs** - 40+ documented endpoints + +## Files + +### Collection & Environment Files +- **F123Dashboard.postman_collection.json** - Complete API collection with all endpoints +- **F123Dashboard.postman_environment.json** - Local development environment variables +- **F123Dashboard.postman_environment.prod.json** - Production environment variables (template) + +### Documentation Files +- **POSTMAN_README.md** - This file - Comprehensive guide +- **POSTMAN_QUICK_GUIDE.md** - Visual quick reference with diagrams +- **POSTMAN_AUTO_AUTH_IMPLEMENTATION.md** - Technical implementation details +- **POSTMAN_FLOW_DIAGRAMS.md** - Visual authentication flows +- **POSTMAN_FILE_INDEX.md** - Navigation guide for all documentation + +## Quick Start + +### 1. Import into Postman + +#### Option A: Import via Postman App +1. Open Postman +2. Click "Import" button (top left) +3. Select all three JSON files: + - `F123Dashboard.postman_collection.json` + - `F123Dashboard.postman_environment.json` + - `F123Dashboard.postman_environment.prod.json` +4. Click "Import" + +#### Option B: Import via URL (if hosted on GitHub) +1. Open Postman +2. Click "Import" → "Link" +3. Paste the raw GitHub URL of the collection file +4. Click "Continue" and "Import" + +### 2. Select Environment + +1. In Postman, look at the top-right corner +2. Select "F123 Dashboard - Local" from the environment dropdown +3. The environment variables will be automatically configured + +### 3. Configure Environment Variables + +Click the eye icon (👁️) next to the environment selector to view/edit variables: + +- **base_url**: API server URL (default: `http://localhost:3000`) +- **jwt_token**: JWT token (auto-populated after login) +- **user_id**: User ID (auto-populated after login) +- **season_id**: Season ID for queries (default: `1`) +- **is_admin**: Admin flag (auto-populated after login) +- **auto_login_username**: Username for auto-authentication (optional) +- **auto_login_password**: Password for auto-authentication (optional) + +### 4. Enable Auto-Authentication (Optional but Recommended) + +To enable automatic JWT token management: + +1. Click the eye icon (👁️) next to environment selector +2. Edit the following variables: + - **auto_login_username**: Your test username (e.g., `testuser`) + - **auto_login_password**: Your test password (e.g., `password123`) +3. Click "Save" + +**Benefits of Auto-Authentication:** +- Automatically logs in when token is missing or expired +- Validates token before each request +- Seamlessly refreshes authentication +- No manual token management needed + +## Collection Structure + +The collection is organized into the following folders: + +### 🏥 Health Check +- **Health Check** - Verify server is running + +### 🔐 Authentication +- **Public** - No authentication required + - Login (auto-saves JWT token) + - Register + - Validate Token +- **Protected** - Requires JWT token + - Refresh Token + - Logout + - Logout All Sessions + - Get User Sessions + - Change Password + - Update User Info +- **Admin Only** - Requires admin privileges + - Get All Users + - Admin Change Password + - Cleanup Expired Sessions + +### 💾 Database +- **Drivers** - Driver information + - Get All Drivers + - Get Drivers Data +- **Championship** - Championship standings + - Get Championship + - Get Cumulative Points +- **Tracks** - Track information + - Get All Tracks +- **Race Results** - Race results + - Get Race Results +- **Seasons** - Season information + - Get All Seasons +- **Constructors** - Team information + - Get Constructors + - Get Constructor Grand Prix Points +- **Admin** - Admin operations + - Set GP Result + +### 🏎️ Fanta (Fantasy) +- Get Fanta Vote +- Set Fanta Vote + +### 📺 Twitch +- Get Stream Info + +### 🎮 Playground +- Get Playground Leaderboard +- Set User Best Score + +## Usage Guide + +### 🚀 Auto-Authentication Features + +This collection includes **intelligent auto-authentication** that makes testing seamless: + +#### Collection-Level Pre-Request Script +Before **every** request (except login/register), the collection automatically: +1. **Checks if JWT token exists** +2. **Validates the token** by calling `/api/auth/validate-token` +3. **Auto-logs in** if token is missing or expired (when credentials are configured) +4. **Proceeds with the request** using the valid token + +#### Collection-Level Test Script +After **every** response, the collection automatically: +1. **Extracts and saves JWT tokens** from responses +2. **Updates user information** (ID, admin status) +3. **Handles 401 errors** by clearing invalid tokens +4. **Logs authentication status** in the console + +#### How to Enable Auto-Login + +**Method 1: Automatic (Run Login Once)** +1. Edit the Login request body with your credentials +2. Run the Login request once +3. The collection saves your credentials automatically +4. All future requests will auto-authenticate + +**Method 2: Manual Configuration** +1. Click the eye icon (👁️) → Edit collection variables +2. Set `auto_login_username` (e.g., `testuser`) +3. Set `auto_login_password` (e.g., `password123`) +4. Save + +**Benefits:** +- ✅ No manual token copying +- ✅ No expired token errors +- ✅ Seamless testing workflow +- ✅ Works with Collection Runner +- ✅ Works with Newman CLI + +### Authentication Flow + +#### Option A: Manual Login (Traditional) + +1. **Register a new user** (optional): + + ```http + POST /api/auth/register + ``` + +2. **Login**: + + ```http + POST /api/auth/login + ``` + + The collection automatically saves the JWT token and user ID to variables. + +3. **Use protected endpoints**: + + The collection uses Bearer token authentication automatically. + +#### Option B: Auto-Login (Recommended) + +1. **Configure credentials** (see above) +2. **Run any request** - authentication happens automatically +3. **Test freely** - no manual token management needed + +### Testing Protected Endpoints + +1. Run the "Login" request first +2. The JWT token is automatically saved +3. All subsequent protected requests will use this token +4. Token is sent in the Authorization header as: `Bearer {{jwt_token}}` + +### Testing Admin Endpoints + +1. Login with an admin account +2. The same JWT token is used +3. Admin middleware checks the `is_admin` flag in the database + +### Working with Seasons + +Most endpoints accept an optional `seasonId` parameter: + +```json +{ + "seasonId": 1 +} +``` + +You can change the default season in the environment variables. + +## Environment Variables + +### Collection Variables +These are set at the collection level and shared across all requests: + +- `base_url` - API base URL +- `jwt_token` - JWT authentication token (auto-populated) +- `user_id` - Current user ID (auto-populated) +- `season_id` - Default season ID + +### Using Variables in Requests + +Variables are referenced using double curly braces: +- URL: `{{base_url}}/api/auth/login` +- Body: `"userId": {{user_id}}` + +## Request Examples + +### Login Example +```json +{ + "username": "testuser", + "password": "password123" +} +``` + +### Register Example +```json +{ + "username": "newuser", + "password": "password123", + "name": "John", + "surname": "Doe" +} +``` + +### Set Fantasy Vote Example +```json +{ + "userId": 1, + "raceId": 1, + "seasonId": 1, + "1_place_id": 1, + "2_place_id": 2, + "3_place_id": 3, + "4_place_id": 4, + "5_place_id": 5, + "6_place_id": 6, + "7_place_id": 7, + "8_place_id": 8, + "fast_lap_id": 1, + "dnf_id": 9, + "team_id": 1 +} +``` + +## 🤖 Auto-Authentication Deep Dive + +### How It Works + +The collection uses Postman's event scripts for intelligent authentication management: + +#### Pre-Request Script (Runs before EVERY request) + +```javascript +// 1. Check if current request is an auth endpoint (skip auto-login) +// 2. Get JWT token from collection variables +// 3. If no token exists → Auto-login +// 4. If token exists → Validate it +// 5. If token invalid/expired → Auto-login +// 6. Proceed with request +``` + +**Validation Flow:** +``` +Request → Has Token? → Validate Token → Valid? → Use Token + ↓ ↓ + Auto-Login ← Need Credentials ← Invalid +``` + +#### Test Script (Runs after EVERY response) + +```javascript +// 1. Check if response contains JWT token → Save it +// 2. Check if response contains user info → Save it +// 3. If 401 Unauthorized → Clear invalid token +// 4. Log authentication status +``` + +### Console Output + +When auto-authentication is working, you'll see: + +``` +✅ Token is valid +``` + +Or when auto-login triggers: + +``` +No JWT token found. Attempting auto-login... +✅ Auto-login successful! Token saved. +User ID: 1 +Is Admin: true +``` + +### Credential Storage + +**Security Note:** Credentials are stored in collection/environment variables. For production testing: +- Use environment-specific variables +- Don't commit credentials to version control +- Consider using Postman Vault for sensitive data + +## Tips & Best Practices + +### 1. Enhanced Login Test Script + +The Login request includes comprehensive test scripts: +```javascript +if (pm.response.code === 200) { + const response = pm.response.json(); + if (response.success && response.jwtToken) { + pm.collectionVariables.set('jwt_token', response.jwtToken); + pm.collectionVariables.set('user_id', response.user.id); + } +} +``` + +### 2. Running Collections +You can run the entire collection or folders using the Collection Runner: +1. Click "..." next to collection name +2. Select "Run collection" +3. Choose which folders/requests to run +4. Set iterations and delays as needed + +### 3. Automated Testing +Add tests to verify responses: +```javascript +pm.test("Status code is 200", function () { + pm.response.to.have.status(200); +}); + +pm.test("Response has success field", function () { + pm.expect(pm.response.json()).to.have.property('success'); +}); +``` + +### 4. Pre-request Scripts +Use pre-request scripts to set up data before sending requests: +```javascript +pm.collectionVariables.set("timestamp", Date.now()); +``` + +### 5. Multiple Environments +Switch between local, staging, and production by selecting different environments: +- F123 Dashboard - Local (localhost:3000) +- F123 Dashboard - Production (your production URL) + +## Troubleshooting + +### "401 Unauthorized" errors +- Ensure you've logged in first +- Check that JWT token is saved in environment variables +- Token may have expired - login again + +### "403 Forbidden" errors +- Endpoint requires admin privileges +- Login with an admin account + +### Connection errors +- Verify the server is running on the specified port +- Check `base_url` in environment variables +- Ensure there are no CORS issues + +### Invalid season ID +- Check the `season_id` environment variable +- Use "Get All Seasons" to see available seasons + +## Advanced Features + +### Newman (CLI) +Run collections from command line using Newman: + +```bash +npm install -g newman + +# Run collection +newman run F123Dashboard.postman_collection.json \ + -e F123Dashboard.postman_environment.json + +# Generate HTML report +newman run F123Dashboard.postman_collection.json \ + -e F123Dashboard.postman_environment.json \ + -r html +``` + +### CI/CD Integration +Integrate Postman tests in your CI/CD pipeline using Newman. + +### Documentation +Generate API documentation from the collection: +1. Click collection name +2. Select "View documentation" +3. Click "Publish" to create public docs + +## Support + +For issues or questions: +- Check the server logs for detailed error messages +- Verify database connection in server configuration +- Review the API documentation in `docs/` folder + +## API Endpoints Summary + +| Method | Endpoint | Auth | Description | +|--------|----------|------|-------------| +| GET | `/api/health` | None | Health check | +| POST | `/api/auth/login` | None | User login | +| POST | `/api/auth/register` | None | User registration | +| POST | `/api/auth/validate-token` | None | Validate JWT | +| POST | `/api/auth/refresh-token` | User | Refresh JWT | +| POST | `/api/auth/logout` | User | Logout current session | +| POST | `/api/auth/logout-all` | User | Logout all sessions | +| POST | `/api/auth/sessions` | User | Get user sessions | +| POST | `/api/auth/change-password` | User | Change password | +| POST | `/api/auth/update-user-info` | User | Update user info | +| POST | `/api/auth/users` | Admin | Get all users | +| POST | `/api/auth/admin-change-password` | Admin | Change any user password | +| POST | `/api/auth/cleanup-sessions` | Admin | Cleanup expired sessions | +| POST | `/api/database/drivers` | None | Get all drivers | +| POST | `/api/database/drivers-data` | None | Get drivers data | +| POST | `/api/database/championship` | None | Get championship | +| POST | `/api/database/cumulative-points` | None | Get cumulative points | +| POST | `/api/database/tracks` | None | Get all tracks | +| POST | `/api/database/race-results` | None | Get race results | +| POST | `/api/database/seasons` | None | Get all seasons | +| POST | `/api/database/constructors` | None | Get constructors | +| POST | `/api/database/constructor-grand-prix-points` | None | Get constructor GP points | +| POST | `/api/database/set-gp-result` | Admin | Set GP result | +| POST | `/api/fanta/votes` | User | Get fantasy votes | +| POST | `/api/fanta/set-vote` | User | Set fantasy vote | +| POST | `/api/twitch/stream-info` | None | Get stream info | +| POST | `/api/playground/leaderboard` | None | Get leaderboard | +| POST | `/api/playground/score` | User | Set best score | + +--- + +**Last Updated**: November 2025 +**Version**: 1.0.0 diff --git a/server/docs/twitch-service.md b/server/docs/twitch-service.md new file mode 100644 index 000000000..28390253a --- /dev/null +++ b/server/docs/twitch-service.md @@ -0,0 +1,295 @@ +# DreandosTwitchInterface Documentation + +## Overview + +The `DreandosTwitchInterface` class provides integration with the Twitch API to retrieve stream information. It handles authentication with Twitch's API and fetches live stream data for specified channels. + +## Features + +- Twitch API authentication using OAuth 2.0 Client Credentials flow +- Stream information retrieval for specific channels +- Automatic token management with expiration handling +- Error handling for API requests + +## Configuration + +The service requires the following configuration: + +- `clientId`: Twitch Client ID (hardcoded in the service) +- `clientSecret`: Retrieved from `RACEFORFEDERICA_DREANDOS_SECRET` environment variable +- `tokenUrl`: Twitch OAuth token endpoint +- `apiUrl`: Twitch API base URL + +## Authentication + +The service uses OAuth 2.0 Client Credentials flow: + +1. Exchanges client credentials for access token +2. Caches token with expiration time +3. Automatically refreshes token when expired + +## Methods + +### Constructor + +```typescript +constructor() +``` + +Initializes the service with Twitch API configuration: + +- Sets up client ID and secret +- Configures API endpoints +- Initializes token management variables + +### Private Methods + +#### `getAccessToken(): Promise` + +Handles Twitch API authentication and token management. + +**Returns:** +- Promise resolving to access token string + +**Functionality:** +- Checks if current token is valid and not expired +- Requests new token if needed using client credentials +- Caches token with expiration time +- Uses `client_credentials` grant type + +**Token Request:** +- Endpoint: `https://id.twitch.tv/oauth2/token` +- Method: POST +- Content-Type: `application/x-www-form-urlencoded` +- Parameters: + - `client_id`: Twitch Client ID + - `client_secret`: Twitch Client Secret + - `grant_type`: "client_credentials" + +**Error Handling:** +- Throws error if client secret is not configured +- Throws error if token request fails +- Includes detailed error messages + +**Example Token Response:** +```json +{ + "access_token": "abcdef123456", + "expires_in": 3600 +} +``` + +### Public Methods + +#### `getStreamInfo(channelName: string): Promise` + +Retrieves stream information for a specific Twitch channel. + +**Parameters:** +- `channelName: string` - Twitch channel username + +**Returns:** +- Promise resolving to JSON string containing stream data + +**API Request:** +- Endpoint: `https://api.twitch.tv/helix/streams?user_login={channelName}` +- Method: GET +- Headers: + - `Client-ID`: Twitch Client ID + - `Authorization`: Bearer {access_token} + +**Response Format:** +The method returns a JSON string containing Twitch API response with stream data. + +**Stream Data Fields:** +- `data`: Array of stream objects (empty if not live) + - `id`: Stream ID + - `user_id`: Channel user ID + - `user_login`: Channel username + - `type`: Stream type (usually "live") + - `title`: Stream title + - `viewer_count`: Current viewer count + - `started_at`: Stream start time (ISO 8601) + - `language`: Stream language + - `thumbnail_url`: Stream thumbnail URL template + +**Example Response (Live Stream):** +```json +{ + "data": [ + { + "id": "123456789", + "user_id": "987654321", + "user_login": "channelname", + "type": "live", + "title": "Amazing F1 Race!", + "viewer_count": 1500, + "started_at": "2023-01-01T12:00:00Z", + "language": "en", + "thumbnail_url": "https://static-cdn.jtvnw.net/previews-ttv/live_user_channelname-{width}x{height}.jpg" + } + ] +} +``` + +**Example Response (Offline Stream):** +```json +{ + "data": [] +} +``` + +**Error Handling:** +- Automatically handles token refresh if needed +- Throws error if authentication fails +- Throws error if stream info request fails +- Includes detailed error messages + +**Example Usage:** +```typescript +const twitchService = new DreandosTwitchInterface(); + +try { + const streamInfo = await twitchService.getStreamInfo('channelname'); + const streamData = JSON.parse(streamInfo); + + if (streamData.data.length > 0) { + console.log('Channel is live!'); + console.log('Title:', streamData.data[0].title); + console.log('Viewers:', streamData.data[0].viewer_count); + } else { + console.log('Channel is offline'); + } +} catch (error) { + console.error('Error fetching stream info:', error); +} +``` + +## TypeScript Interfaces + +### TwitchTokenResponse +```typescript +interface TwitchTokenResponse { + access_token: string; + expires_in: number; +} +``` + +### TwitchStreamResponse +```typescript +interface TwitchStreamResponse { + data: Array<{ + id: string; + user_id: string; + user_login: string; + type: string; + title: string; + viewer_count: number; + started_at: string; + language: string; + thumbnail_url: string; + }>; +} +``` + +## Error Handling + +The service implements comprehensive error handling: + +- Environment variable validation +- API request error handling +- Token refresh error handling +- Detailed error messages for debugging + +Common error scenarios: +- Missing client secret environment variable +- Invalid client credentials +- Network connectivity issues +- Invalid channel names +- Rate limiting from Twitch API + +## Security Considerations + +- Client secret is stored as environment variable +- Access tokens are cached in memory only +- Uses HTTPS for all API requests +- Client ID is hardcoded (consider making it configurable) + +## Rate Limiting + +Twitch API has rate limits: +- Client ID rate limit: 800 requests per minute +- Consider implementing request throttling for high-volume applications +- Monitor API usage to avoid hitting limits + +## Environment Variables + +Required environment variables: +- `RACEFORFEDERICA_DREANDOS_SECRET`: Twitch Client Secret + +## Dependencies + +- `axios`: HTTP client for API requests +- `@genezio/types`: Genezio deployment types + +## Usage Example + +```typescript +const twitchService = new DreandosTwitchInterface(); + +// Check if a channel is live +const checkStream = async (channelName: string) => { + try { + const streamInfo = await twitchService.getStreamInfo(channelName); + const data = JSON.parse(streamInfo); + + if (data.data.length > 0) { + const stream = data.data[0]; + return { + isLive: true, + title: stream.title, + viewers: stream.viewer_count, + startedAt: stream.started_at, + thumbnailUrl: stream.thumbnail_url + }; + } else { + return { + isLive: false + }; + } + } catch (error) { + console.error('Error checking stream:', error); + return { + isLive: false, + error: error.message + }; + } +}; + +// Usage +const streamStatus = await checkStream('dreandos'); +``` + +## Future Improvements + +1. Make client ID configurable through environment variables +2. Implement request caching to reduce API calls +3. Add support for multiple channels in single request +4. Implement webhook support for real-time updates +5. Add retry logic for failed requests +6. Implement proper logging +7. Add support for other Twitch API endpoints +8. Implement rate limiting protection +9. Add user authentication support +10. Cache stream data for better performance + +## Deployment + +The service is deployed using the `@GenezioDeploy()` decorator and can be called from the client application through the Genezio SDK. + +## Twitch API Documentation + +For more information about Twitch API: +- [Twitch API Reference](https://dev.twitch.tv/docs/api/) +- [Authentication Guide](https://dev.twitch.tv/docs/authentication/) +- [Streams API](https://dev.twitch.tv/docs/api/reference#get-streams) diff --git a/server/docs/winston-logging.md b/server/docs/winston-logging.md new file mode 100644 index 000000000..9146b8838 --- /dev/null +++ b/server/docs/winston-logging.md @@ -0,0 +1,312 @@ +# Winston Logging Implementation + +## Overview + +The Express.js backend now uses Winston for structured, professional logging across all services. Winston provides flexible log levels, formatted output, and supports both console and file transports. + +## Features + +- **Structured Logging**: JSON-formatted logs with timestamps and metadata +- **Multiple Log Levels**: error, warn, info, http, debug +- **Console Output**: Colorized console logs for development +- **File Output**: Persistent logs in production (error.log, combined.log) +- **HTTP Request Logging**: Integrated with Morgan middleware +- **Log Rotation**: Automatic rotation when files reach 5MB (max 5 files) +- **Environment-Aware**: Different behavior in development vs production + +## Log Levels + +Winston uses the following log levels (in order of priority): + +1. **error** (0): Critical errors that need immediate attention +2. **warn** (1): Warning messages for potentially harmful situations +3. **info** (2): Informational messages about application state +4. **http** (3): HTTP request/response logs +5. **debug** (4): Detailed debug information for development + +## Configuration + +### Environment Variables + +Add to your `.env` file: + +```env +LOG_LEVEL=debug # error, warn, info, http, or debug +NODE_ENV=development # development or production +``` + +### Log Level Behavior + +- **Development** (default): `debug` - shows all logs +- **Production**: `info` - shows info, warn, and error logs + +## Usage + +### In Services and Controllers + +```typescript +import logger from '../config/logger.js'; + +// Error logging +logger.error('Database connection failed', { + error: err.message, + stack: err.stack, + context: 'additional data' +}); + +// Warning logging +logger.warn('API rate limit approaching', { + current: 95, + limit: 100 +}); + +// Info logging +logger.info('Server started successfully', { + port: 3000, + environment: 'production' +}); + +// HTTP logging (handled automatically by Morgan) +// No manual HTTP logging needed - Morgan handles this + +// Debug logging +logger.debug('Processing user request', { + userId: 123, + action: 'updateProfile' +}); +``` + +### Best Practices + +#### ✅ DO: + +```typescript +// Include context and structured data +logger.error('Failed to save user', { + userId: user.id, + error: err.message, + stack: err.stack +}); + +// Use appropriate log levels +logger.warn('Cache miss', { key: cacheKey }); +logger.info('User logged in', { userId: user.id }); +logger.debug('Cache state', { size: cache.size, hits: cache.hits }); + +// Log errors with stack traces +logger.error('Unexpected error', { + error: err instanceof Error ? err.message : 'Unknown error', + stack: err instanceof Error ? err.stack : undefined +}); +``` + +#### ❌ DON'T: + +```typescript +// Don't use console.log/console.error +console.log('User logged in'); // ❌ + +// Don't log sensitive data +logger.info('User login', { password: user.password }); // ❌ + +// Don't use string concatenation +logger.error('Error: ' + err.message); // ❌ Use structured data instead + +// Don't log without context +logger.error(err); // ❌ Include metadata +``` + +## Log Formats + +### Development (Console) + +Colorized output with readable format: +``` +2025-11-14 10:45:23 [info]: 🚀 Server running on http://localhost:3000 +2025-11-14 10:45:24 [http]: POST /api/auth/login 200 245 - 125.5 ms +2025-11-14 10:45:25 [error]: Database query failed + Error: Connection timeout + at Pool.query (...) +``` + +### Production (Files) + +JSON format for log aggregation tools: +```json +{ + "level": "error", + "message": "Database query failed", + "timestamp": "2025-11-14 10:45:25", + "error": "Connection timeout", + "stack": "Error: Connection timeout\n at Pool.query (...)", + "userId": 123 +} +``` + +## Log Files (Production Only) + +Logs are stored in `server/logs/` directory: + +- **error.log**: Error-level logs only +- **combined.log**: All logs (info, warn, error, http) + +Both files: +- Max size: 5MB +- Max files: 5 (automatic rotation) +- Format: JSON for easy parsing + +## HTTP Request Logging + +Morgan middleware automatically logs all HTTP requests: + +``` +POST /api/auth/login 200 245 - 125.5 ms +GET /api/database/drivers 200 1024 - 45.2 ms +POST /api/fanta/votes 401 - - 12.1 ms +``` + +Format: `METHOD PATH STATUS CONTENT_LENGTH - RESPONSE_TIME` + +## Integration Points + +Winston logging is integrated throughout the application: + +### 1. Server Startup (`server.ts`) +```typescript +logger.info('🚀 Server running on http://localhost:3000'); +logger.info('📦 Environment: development'); +logger.info('🔧 Health check: http://localhost:3000/api/health'); +``` + +### 2. Database Connection (`config/db.ts`) +```typescript +logger.info('Database connected successfully', { timestamp: res.rows[0].now }); +logger.error('Database connection error', { error: err.message, stack: err.stack }); +``` + +### 3. Authentication Middleware (`middleware/auth.middleware.ts`) +```typescript +logger.error('Auth middleware error', { + error: error.message, + stack: error.stack +}); +``` + +### 4. All Controllers +```typescript +logger.error('Error getting users:', error); +logger.error('Error during login:', error); +logger.error('Error setting fanta vote:', error); +// etc. +``` + +## Monitoring and Analysis + +### Viewing Logs in Development + +Logs appear in the console with colors: +```bash +npm run dev +``` + +### Viewing Logs in Production + +```bash +# View error logs +tail -f server/logs/error.log + +# View all logs +tail -f server/logs/combined.log + +# Search for specific errors +grep "Database" server/logs/error.log + +# View logs in JSON format +cat server/logs/combined.log | jq '.' +``` + +### Log Aggregation + +Production logs are in JSON format, making them compatible with: +- **ELK Stack** (Elasticsearch, Logstash, Kibana) +- **Splunk** +- **Datadog** +- **CloudWatch** (AWS) +- **Stackdriver** (GCP) + +Example Logstash configuration: +```json +{ + "input": { + "file": { + "path": "/path/to/server/logs/combined.log", + "codec": "json" + } + } +} +``` + +## Troubleshooting + +### No logs appearing in console + +Check `LOG_LEVEL` environment variable: +```bash +echo $LOG_LEVEL +``` + +Set to `debug` for maximum verbosity: +```env +LOG_LEVEL=debug +``` + +### Log files not being created + +1. Ensure `NODE_ENV=production`: +```env +NODE_ENV=production +``` + +2. Check `server/logs/` directory exists: +```bash +mkdir -p server/logs +``` + +3. Check file permissions: +```bash +ls -la server/logs/ +``` + +### Too many logs + +Reduce log level in production: +```env +LOG_LEVEL=warn # Only warnings and errors +``` + +## Performance Considerations + +- **Console logging**: Minimal performance impact in development +- **File logging**: Asynchronous writes in production (non-blocking) +- **Log rotation**: Automatic cleanup prevents disk space issues +- **Structured data**: Use objects for metadata, not string concatenation + +## Security + +⚠️ **Never log sensitive data**: +- Passwords (plain or hashed) +- JWT tokens +- API keys +- Credit card numbers +- Personal identification numbers + +✅ **Safe to log**: +- User IDs +- Request methods and paths +- Response status codes +- Error messages (without sensitive details) +- Timestamps and durations + +--- + +**Summary**: Winston provides production-ready logging with minimal configuration. Use structured logging with appropriate levels and context for effective debugging and monitoring. diff --git a/server/package.json b/server/package.json new file mode 100644 index 000000000..41d3de40f --- /dev/null +++ b/server/package.json @@ -0,0 +1,46 @@ +{ + "name": "server", + "version": "1.0.0", + "type": "module", + "main": "index.js", + "scripts": { + "dev": "tsx watch src/server.ts", + "build": "tsc", + "start": "node dist/server.js", + "start:dev": "NODE_ENV=development tsx src/server.ts", + "test": "vitest" + }, + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.5", + "@types/morgan": "^1.9.10", + "@types/node": "^22.19.9", + "@types/node-cron": "^3.0.11", + "@types/nodemailer": "^7.0.4", + "@types/pg": "^8.15.6", + "@types/supertest": "^6.0.3", + "supertest": "^7.2.2", + "ts-node": "^10.9.2", + "tsx": "^4.20.6", + "typescript": "^5.6.3", + "vitest": "^4.0.17" + }, + "dependencies": { + "@f123dashboard/shared": "file:../shared", + "@types/bcrypt": "^6.0.0", + "@types/jsonwebtoken": "^9.0.10", + "axios": "^1.8.4", + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.1", + "node-cron": "^4.2.1", + "nodemailer": "^8.0.0", + "pg": "^8.13.1", + "winston": "^3.18.3" + } +} diff --git a/server/scripts/alll_race_points.sql b/server/scripts/alll_race_points.sql new file mode 100644 index 000000000..ac8aca7d0 --- /dev/null +++ b/server/scripts/alll_race_points.sql @@ -0,0 +1,172 @@ +-- public.all_race_points source + +CREATE OR REPLACE VIEW public.all_race_points +AS WITH all_race_points AS ( + SELECT rre.race_results_id AS result_id, + rre.pilot_id AS driver_id, + CASE rre."position" + WHEN 1 THEN st."1_points" + WHEN 2 THEN st."2_points" + WHEN 3 THEN st."3_points" + WHEN 4 THEN st."4_points" + WHEN 5 THEN st."5_points" + WHEN 6 THEN st."6_points" + ELSE 0 + END AS race_point, + CASE + WHEN rre.fast_lap THEN st.fast_lap_points + ELSE 0 + END AS fast_lap_point + FROM race_result_entries rre + JOIN session_type st ON st.id = 1 + ), all_full_race_points AS ( + SELECT frre.race_results_id AS result_id, + frre.pilot_id AS driver_id, + CASE frre."position" + WHEN 1 THEN st."1_points" + WHEN 2 THEN st."2_points" + WHEN 3 THEN st."3_points" + WHEN 4 THEN st."4_points" + WHEN 5 THEN st."5_points" + WHEN 6 THEN st."6_points" + ELSE 0 + END AS full_race_point, + CASE + WHEN frre.fast_lap THEN st.fast_lap_points + ELSE 0 + END AS fast_lap_point + FROM full_race_result_entries frre + JOIN session_type st ON st.id = 5 + ), all_sprint_points AS ( + SELECT sre.sprint_results_id AS result_id, + sre.pilot_id AS driver_id, + CASE sre."position" + WHEN 1 THEN st."1_points" + WHEN 2 THEN st."2_points" + WHEN 3 THEN st."3_points" + WHEN 4 THEN st."4_points" + WHEN 5 THEN st."5_points" + WHEN 6 THEN st."6_points" + ELSE 0 + END AS sprint_point, + CASE + WHEN sre.fast_lap THEN st.fast_lap_points + ELSE 0 + END AS fast_lap_point + FROM sprint_result_entries sre + JOIN session_type st ON st.id = 4 + ), all_qualifying_points AS ( + SELECT qre.qualifying_results_id AS result_id, + qre.pilot_id AS driver_id, + CASE qre."position" + WHEN 1 THEN st."1_points" + WHEN 2 THEN st."2_points" + WHEN 3 THEN st."3_points" + WHEN 4 THEN st."4_points" + WHEN 5 THEN st."5_points" + WHEN 6 THEN st."6_points" + ELSE 0 + END AS qualifying_point, + 0 AS fast_lap_point + FROM qualifying_result_entries qre + JOIN session_type st ON st.id = 2 + ), all_free_practice_points AS ( + SELECT fpre.free_practice_results_id AS result_id, + fpre.pilot_id AS driver_id, + CASE fpre."position" + WHEN 1 THEN st."1_points" + WHEN 2 THEN st."2_points" + WHEN 3 THEN st."3_points" + WHEN 4 THEN st."4_points" + WHEN 5 THEN st."5_points" + WHEN 6 THEN st."6_points" + ELSE 0 + END AS free_practice_point, + 0 AS fast_lap_point + FROM free_practice_result_entries fpre + JOIN session_type st ON st.id = 3 + ), all_drivers AS ( + SELECT drivers.id AS driver_id, + drivers.username AS driver_username, + drivers.name AS driver_name, + drivers.surname AS driver_surname, + drivers.description AS driver_description, + drivers.license_pt AS driver_license_pt, + drivers.consistency_pt AS driver_consistency_pt, + drivers.fast_lap_pt AS driver_fast_lap_pt, + drivers.dangerous_pt AS drivers_dangerous_pt, + drivers.ingenuity_pt AS driver_ingenuity_pt, + drivers.strategy_pt AS driver_strategy_pt, + drivers.color AS driver_color, + inner_table_1.pilot_name, + inner_table_1.pilot_surname, + inner_table_1.car_name, + inner_table_1.car_overall_score, + drivers.season AS season_id + FROM drivers + JOIN ( SELECT pilots.id AS pilot_id, + pilots.name AS pilot_name, + pilots.surname AS pilot_surname, + cars.name AS car_name, + cars.overall_score AS car_overall_score + FROM pilots + JOIN cars ON pilots.car_id = cars.id) inner_table_1 ON drivers.pilot_id = inner_table_1.pilot_id + ) + SELECT all_drivers.driver_id, + all_drivers.driver_username, + all_drivers.driver_name, + all_drivers.driver_surname, + all_drivers.driver_description, + all_drivers.driver_license_pt, + all_drivers.driver_consistency_pt, + all_drivers.driver_fast_lap_pt, + all_drivers.drivers_dangerous_pt, + all_drivers.driver_ingenuity_pt, + all_drivers.driver_strategy_pt, + all_drivers.driver_color, + all_drivers.pilot_name, + all_drivers.pilot_surname, + all_drivers.car_name, + all_drivers.car_overall_score, + all_drivers.season_id, + COALESCE(inner_table.total_sprint_points, 0) AS total_sprint_points, + COALESCE(inner_table.total_free_practice_points, 0) AS total_free_practice_points, + COALESCE(inner_table.total_qualifying_points, 0) AS total_qualifying_points, + COALESCE(inner_table.total_full_race_points, 0) AS total_full_race_points, + COALESCE(inner_table.total_race_points, 0) AS total_race_points, + COALESCE(inner_table.total_full_race_points, 0) + COALESCE(inner_table.total_free_practice_points, 0) + COALESCE(inner_table.total_qualifying_points, 0) + COALESCE(inner_table.total_sprint_points, 0) + COALESCE(inner_table.total_race_points, 0) AS total_points + FROM all_drivers + LEFT JOIN ( SELECT + COALESCE(rp.driver_id, frp.driver_id, sp.driver_id, qp.driver_id, fpp.driver_id) AS driver_id, + COALESCE(rp.total_race_points, 0) AS total_race_points, + COALESCE(frp.total_full_race_points, 0) AS total_full_race_points, + COALESCE(sp.total_sprint_points, 0) AS total_sprint_points, + COALESCE(qp.total_qualifying_points, 0) AS total_qualifying_points, + COALESCE(fpp.total_free_practice_points, 0) AS total_free_practice_points + FROM ( + SELECT driver_id, sum(race_point + fast_lap_point) AS total_race_points + FROM all_race_points + GROUP BY driver_id + ) rp + FULL OUTER JOIN ( + SELECT driver_id, sum(full_race_point + fast_lap_point) AS total_full_race_points + FROM all_full_race_points + GROUP BY driver_id + ) frp ON rp.driver_id = frp.driver_id + FULL OUTER JOIN ( + SELECT driver_id, sum(sprint_point + fast_lap_point) AS total_sprint_points + FROM all_sprint_points + GROUP BY driver_id + ) sp ON COALESCE(rp.driver_id, frp.driver_id) = sp.driver_id + FULL OUTER JOIN ( + SELECT driver_id, sum(qualifying_point) AS total_qualifying_points + FROM all_qualifying_points + GROUP BY driver_id + ) qp ON COALESCE(rp.driver_id, frp.driver_id, sp.driver_id) = qp.driver_id + FULL OUTER JOIN ( + SELECT driver_id, sum(free_practice_point) AS total_free_practice_points + FROM all_free_practice_points + GROUP BY driver_id + ) fpp ON COALESCE(rp.driver_id, frp.driver_id, sp.driver_id, qp.driver_id) = fpp.driver_id + ) inner_table ON all_drivers.driver_id = inner_table.driver_id + ORDER BY all_drivers.season_id, (inner_table.total_full_race_points + inner_table.total_free_practice_points + inner_table.total_qualifying_points + inner_table.total_sprint_points + inner_table.total_race_points) DESC; \ No newline at end of file diff --git a/server/scripts/auth_migration.sql b/server/scripts/auth_migration.sql new file mode 100644 index 000000000..ad563b572 --- /dev/null +++ b/server/scripts/auth_migration.sql @@ -0,0 +1,40 @@ +-- Authentication improvements for fanta_player table + +-- Add new columns for proper authentication +ALTER TABLE fanta_player +ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT NOW(), +ADD COLUMN IF NOT EXISTS last_login TIMESTAMP, +ADD COLUMN IF NOT EXISTS password_updated_at TIMESTAMP DEFAULT NOW(), +ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT TRUE; + +-- Create indexes for better performance +CREATE INDEX IF NOT EXISTS idx_fanta_player_username ON fanta_player(username); + +-- Create table for session management (optional, for more advanced session handling) +CREATE TABLE IF NOT EXISTS user_sessions ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES fanta_player(id) ON DELETE CASCADE, + session_token VARCHAR(255) UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + last_activity TIMESTAMP DEFAULT NOW(), + expires_at TIMESTAMP NOT NULL, + ip_address INET, + user_agent TEXT, + is_active BOOLEAN DEFAULT TRUE +); + +CREATE INDEX IF NOT EXISTS idx_user_sessions_token ON user_sessions(session_token); +CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id); +CREATE INDEX IF NOT EXISTS idx_user_sessions_expires ON user_sessions(expires_at); + +-- Function to clean expired sessions +CREATE OR REPLACE FUNCTION clean_expired_sessions() +RETURNS INTEGER AS $$ +DECLARE + deleted_count INTEGER; +BEGIN + DELETE FROM user_sessions WHERE expires_at < NOW(); + GET DIAGNOSTICS deleted_count = ROW_COUNT; + RETURN deleted_count; +END; +$$ LANGUAGE plpgsql; diff --git a/server/scripts/create_property_table.sql b/server/scripts/create_property_table.sql new file mode 100644 index 000000000..a8d346b02 --- /dev/null +++ b/server/scripts/create_property_table.sql @@ -0,0 +1,24 @@ +-- Create property table for storing application configuration +-- This table allows dynamic configuration of application features +-- without requiring code changes or redeployment + +CREATE TABLE IF NOT EXISTS property ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + description TEXT, + value VARCHAR(500) NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- Add index on name for faster lookups +CREATE INDEX IF NOT EXISTS idx_property_name ON property(name); + +-- Add comment to table +COMMENT ON TABLE property IS 'Stores application configuration properties and feature flags'; +COMMENT ON COLUMN property.id IS 'Primary key'; +COMMENT ON COLUMN property.name IS 'Unique property name identifier'; +COMMENT ON COLUMN property.description IS 'Human-readable description of the property'; +COMMENT ON COLUMN property.value IS 'Property value stored as string (can be parsed as needed)'; +COMMENT ON COLUMN property.created_at IS 'Timestamp when property was created'; +COMMENT ON COLUMN property.updated_at IS 'Timestamp when property was last updated'; diff --git a/server/scripts/dump-raceforfederica-db-ddl-tables-views_Version2.sql b/server/scripts/dump-raceforfederica-db-ddl-tables-views_Version2.sql new file mode 100644 index 000000000..1f6b1ca28 --- /dev/null +++ b/server/scripts/dump-raceforfederica-db-ddl-tables-views_Version2.sql @@ -0,0 +1,158 @@ +-- +-- DDL for Tables and Views extracted from dump-raceforfederica-db-202507131045.sql +-- + +-- TABLES + +CREATE TABLE public."Championship" ( + id integer NOT NULL, + gran_prix_id integer NOT NULL +); + +CREATE TABLE public.cars ( + name text NOT NULL, + overall_score integer NOT NULL, + id bigint NOT NULL +); + +CREATE TABLE public.drivers ( + username text NOT NULL, + name text, + surname text, + id bigint NOT NULL, + free_practice_points integer DEFAULT 0 NOT NULL, + qualifying_points integer DEFAULT 0 NOT NULL, + race_points integer DEFAULT 0 NOT NULL, + pilot_id bigint, + description text, + consistency_pt integer, + fast_lap_pt integer, + dangerous_pt integer, + ingenuity_pt integer, + strategy_pt integer, + color text, + license_pt bigint DEFAULT 3 +); + +CREATE TABLE public.free_practice_result_entries ( + free_practice_results_id integer NOT NULL, + pilot_id integer NOT NULL, + "position" integer NOT NULL +); + +CREATE TABLE public.full_race_result_entries ( + race_results_id integer NOT NULL, + pilot_id integer NOT NULL, + "position" integer NOT NULL, + fast_lap boolean DEFAULT false +); + +CREATE TABLE public.gran_prix ( + id bigint NOT NULL, + date timestamp without time zone NOT NULL, + track_id bigint NOT NULL, + race_results_id bigint, + sprint_results_id bigint, + qualifying_results_id bigint, + free_practice_results_id bigint, + has_sprint bigint, + has_x2 bigint DEFAULT 0, + full_race_results_id bigint, + season_id integer NOT NULL +); + +CREATE TABLE public.qualifying_result_entries ( + qualifying_results_id integer NOT NULL, + pilot_id integer NOT NULL, + "position" integer NOT NULL +); + +CREATE TABLE public.race_result_entries ( + race_results_id integer NOT NULL, + pilot_id integer NOT NULL, + "position" integer NOT NULL, + fast_lap boolean DEFAULT false +); + +CREATE TABLE public.seasons ( + id integer NOT NULL, + description text, + start_date timestamp without time zone NOT NULL, + end_date timestamp without time zone +); + +CREATE TABLE public.session_type ( + "1_points" integer NOT NULL, + "2_points" integer NOT NULL, + "3_points" integer NOT NULL, + "4_points" integer NOT NULL, + "5_points" integer NOT NULL, + id bigint NOT NULL, + name text NOT NULL, + fast_lap_points integer, + "6_points" integer +); + +CREATE TABLE public.sprint_result_entries ( + sprint_results_id integer NOT NULL, + pilot_id integer NOT NULL, + "position" integer NOT NULL, + fast_lap boolean DEFAULT false +); + +CREATE TABLE public.tracks ( + name text, + country text, + id bigint NOT NULL, + length double precision NOT NULL, + besttime_driver_time text NOT NULL, + besttime_driver_id bigint +); + +CREATE TABLE public.fanta ( + id integer NOT NULL, + fanta_player_id bigint NOT NULL, + race_id bigint NOT NULL, + "1_place_id" bigint NOT NULL, + "2_place_id" bigint NOT NULL, + "3_place_id" bigint NOT NULL, + "4_place_id" bigint NOT NULL, + "5_place_id" bigint NOT NULL, + "6_place_id" bigint NOT NULL, + fast_lap_id bigint NOT NULL, + dnf_id bigint DEFAULT 0, + seasion integer +); + +CREATE TABLE public.fanta_player ( + id integer NOT NULL, + username text, + name text, + surname text, + password text, + image bytea, + created_at timestamp without time zone DEFAULT now(), + last_login timestamp without time zone, + password_updated_at timestamp without time zone DEFAULT now(), + is_active boolean DEFAULT true +); + + +CREATE TABLE public.user_sessions ( + id integer NOT NULL, + user_id integer, + session_token character varying(255) NOT NULL, + created_at timestamp without time zone DEFAULT now(), + last_activity timestamp without time zone DEFAULT now(), + expires_at timestamp without time zone NOT NULL, + ip_address inet, + user_agent text, + is_active boolean DEFAULT true +); + +CREATE TABLE public.pilots ( + name text, + surname text, + id bigint NOT NULL, + car_id bigint NOT NULL +); diff --git a/server/scripts/fanta_query_creator.py b/server/scripts/fanta_query_creator.py new file mode 100644 index 000000000..95dc61626 --- /dev/null +++ b/server/scripts/fanta_query_creator.py @@ -0,0 +1,11 @@ +# SCRIPT USED TO CREATE AUTOMATICALLY INSERT SCRIPT FOR FANTA TABLE # +##################################################################### + +num_tracks = 25 +num_players = 2 + +id = 1 +for player in range(1, num_players + 1): + for track in range(1, num_tracks + 1): + print(f"INSERT INTO \"fanta\" (\"id\", \"fanta_player_id\", \"race_id\", \"1_place_id\", \"2_place_id\", \"3_place_id\", \"4_place_id\", \"5_place_id\", \"6_place_id\", \"fast_lap_id\", \"sprint_id\") VALUES ({id}, {player}, {track}, 0, 0, 0, 0, 0, 0, 0, 0);") + id = id + 1 \ No newline at end of file diff --git a/server/scripts/fanta_table_modifications.sql b/server/scripts/fanta_table_modifications.sql new file mode 100644 index 000000000..59b84fe56 --- /dev/null +++ b/server/scripts/fanta_table_modifications.sql @@ -0,0 +1,79 @@ +-- Fanta Table Modifications +-- This script adds new columns and foreign key constraints to the fanta table + +-- 1. Add new columns 7_place_id and 8_place_id +ALTER TABLE fanta +ADD COLUMN "7_place_id" int8 NOT NULL DEFAULT 0, +ADD COLUMN "8_place_id" int8 NOT NULL DEFAULT 0; + +-- 2. Add foreign key constraints for all place positions (1-8) referencing drivers.id +ALTER TABLE fanta +ADD CONSTRAINT fk_fanta_1_place_id +FOREIGN KEY ("1_place_id") REFERENCES drivers(id); + +ALTER TABLE fanta +ADD CONSTRAINT fk_fanta_2_place_id +FOREIGN KEY ("2_place_id") REFERENCES drivers(id); + +ALTER TABLE fanta +ADD CONSTRAINT fk_fanta_3_place_id +FOREIGN KEY ("3_place_id") REFERENCES drivers(id); + +ALTER TABLE fanta +ADD CONSTRAINT fk_fanta_4_place_id +FOREIGN KEY ("4_place_id") REFERENCES drivers(id); + +ALTER TABLE fanta +ADD CONSTRAINT fk_fanta_5_place_id +FOREIGN KEY ("5_place_id") REFERENCES drivers(id); + +ALTER TABLE fanta +ADD CONSTRAINT fk_fanta_6_place_id +FOREIGN KEY ("6_place_id") REFERENCES drivers(id); + +ALTER TABLE fanta +ADD CONSTRAINT fk_fanta_7_place_id +FOREIGN KEY ("7_place_id") REFERENCES drivers(id); + +ALTER TABLE fanta +ADD CONSTRAINT fk_fanta_8_place_id +FOREIGN KEY ("8_place_id") REFERENCES drivers(id); + +-- 3. Add foreign key constraint for fast_lap_id referencing drivers.id +ALTER TABLE fanta +ADD CONSTRAINT fk_fanta_fast_lap_id +FOREIGN KEY (fast_lap_id) REFERENCES drivers(id); + +-- 4. Add foreign key constraint for dnf_id referencing drivers.id +ALTER TABLE fanta +ADD CONSTRAINT fk_fanta_dnf_id +FOREIGN KEY (dnf_id) REFERENCES drivers(id); + +-- 5. Add foreign key constraint for fanta_player_id referencing users.id +ALTER TABLE fanta +ADD CONSTRAINT fk_fanta_player_id +FOREIGN KEY (fanta_player_id) REFERENCES users(id); + +-- 6. Add foreign key constraint for season_id referencing seasons.id +ALTER TABLE fanta +ADD CONSTRAINT fk_fanta_season_id +FOREIGN KEY (season_id) REFERENCES seasons(id); + +-- Optional: Add comments to document the new columns +COMMENT ON COLUMN fanta."7_place_id" IS 'Driver ID for 7th place fantasy team position'; +COMMENT ON COLUMN fanta."8_place_id" IS 'Driver ID for 8th place fantasy team position'; + +-- Optional: Create indexes for better query performance on foreign key columns +CREATE INDEX IF NOT EXISTS idx_fanta_1_place_id ON fanta("1_place_id"); +CREATE INDEX IF NOT EXISTS idx_fanta_2_place_id ON fanta("2_place_id"); +CREATE INDEX IF NOT EXISTS idx_fanta_3_place_id ON fanta("3_place_id"); +CREATE INDEX IF NOT EXISTS idx_fanta_4_place_id ON fanta("4_place_id"); +CREATE INDEX IF NOT EXISTS idx_fanta_5_place_id ON fanta("5_place_id"); +CREATE INDEX IF NOT EXISTS idx_fanta_6_place_id ON fanta("6_place_id"); +CREATE INDEX IF NOT EXISTS idx_fanta_7_place_id ON fanta("7_place_id"); +CREATE INDEX IF NOT EXISTS idx_fanta_8_place_id ON fanta("8_place_id"); +CREATE INDEX IF NOT EXISTS idx_fanta_fast_lap_id ON fanta(fast_lap_id); +CREATE INDEX IF NOT EXISTS idx_fanta_dnf_id ON fanta(dnf_id); +CREATE INDEX IF NOT EXISTS idx_fanta_player_id ON fanta(fanta_player_id); +CREATE INDEX IF NOT EXISTS idx_fanta_season_id ON fanta(season_id); +CREATE INDEX IF NOT EXISTS idx_fanta_race_id ON fanta(race_id); diff --git a/server/scripts/insert_mail_job_property.sql b/server/scripts/insert_mail_job_property.sql new file mode 100644 index 000000000..279696ae1 --- /dev/null +++ b/server/scripts/insert_mail_job_property.sql @@ -0,0 +1,19 @@ +-- Insert property to enable/disable the sendIncomingRaceMail cron job +-- Value: '1' = enabled, '0' = disabled + +INSERT INTO property (name, description, value) +VALUES ( + 'send_incoming_race_mail_enabled', + 'Controls whether the automated email notification for upcoming races is sent. Set to 1 to enable, 0 to disable.', + '1' +) +ON CONFLICT (name) +DO UPDATE SET + description = EXCLUDED.description, + value = EXCLUDED.value, + updated_at = NOW(); + +-- Display the inserted/updated property +SELECT * FROM property WHERE name = 'send_incoming_race_mail_enabled'; + +UPDATE property SET value = '0' WHERE name = 'send_incoming_race_mail_enabled'; \ No newline at end of file diff --git a/server/scripts/migration_script.sql b/server/scripts/migration_script.sql new file mode 100644 index 000000000..03cb5fea6 --- /dev/null +++ b/server/scripts/migration_script.sql @@ -0,0 +1,133 @@ + +CREATE TABLE race_result_entries ( + race_results_id INTEGER NOT NULL REFERENCES race_results(id) ON DELETE CASCADE, + pilot_id INTEGER NOT NULL REFERENCES pilots(id) ON DELETE CASCADE, + season_id INTEGER NOT NULL REFERENCES seasons(id) ON DELETE CASCADE, + position INTEGER NOT NULL, -- 1 for winner, 2 for second, ..., 0 for DNF + fast_lap BOOLEAN DEFAULT FALSE, -- Fastest lap + PRIMARY KEY (race_results_id, pilot_id, season_id) +); + + +INSERT INTO race_result_entries (race_results_id, pilot_id, season_id, position, fast_lap) +SELECT id, "1_place_id", 1, 1, ("fast_lap_id" = "1_place_id") FROM race_results WHERE "1_place_id" IS NOT NULL AND "1_place_id" > 0 +UNION ALL +SELECT id, "2_place_id", 1, 2, ("fast_lap_id" = "2_place_id") FROM race_results WHERE "2_place_id" IS NOT NULL AND "2_place_id" > 0 +UNION ALL +SELECT id, "3_place_id", 1, 3, ("fast_lap_id" = "3_place_id") FROM race_results WHERE "3_place_id" IS NOT NULL AND "3_place_id" > 0 +UNION ALL +SELECT id, "4_place_id", 1, 4, ("fast_lap_id" = "4_place_id") FROM race_results WHERE "4_place_id" IS NOT NULL AND "4_place_id" > 0 +UNION ALL +SELECT id, "5_place_id", 1, 5, ("fast_lap_id" = "5_place_id") FROM race_results WHERE "5_place_id" IS NOT NULL AND "5_place_id" > 0 +UNION ALL +SELECT id, "6_place_id", 1, 6, ("fast_lap_id" = "6_place_id") FROM race_results WHERE "6_place_id" IS NOT NULL AND "6_place_id" > 0; + + +CREATE TABLE sprint_result_entries ( + sprint_results_id INTEGER NOT NULL REFERENCES sprint_results(id) ON DELETE CASCADE, + pilot_id INTEGER NOT NULL REFERENCES pilots(id) ON DELETE CASCADE, + season_id INTEGER NOT NULL REFERENCES seasons(id) ON DELETE CASCADE, + position INTEGER NOT NULL, + fast_lap BOOLEAN DEFAULT FALSE, + PRIMARY KEY (sprint_results_id, pilot_id, season_id) +); + +-- 3. Migrate sprint_results data +INSERT INTO sprint_result_entries (sprint_results_id, pilot_id, season_id, position, fast_lap) +SELECT id, "1_place_id", 1, 1, ("fast_lap_id" = "1_place_id") FROM sprint_results WHERE "1_place_id" IS NOT NULL AND "1_place_id" > 0 +UNION ALL +SELECT id, "2_place_id", 1, 2, ("fast_lap_id" = "2_place_id") FROM sprint_results WHERE "2_place_id" IS NOT NULL AND "2_place_id" > 0 +UNION ALL +SELECT id, "3_place_id", 1, 3, ("fast_lap_id" = "3_place_id") FROM sprint_results WHERE "3_place_id" IS NOT NULL AND "3_place_id" > 0 +UNION ALL +SELECT id, "4_place_id", 1, 4, ("fast_lap_id" = "4_place_id") FROM sprint_results WHERE "4_place_id" IS NOT NULL AND "4_place_id" > 0 +UNION ALL +SELECT id, "5_place_id", 1, 5, ("fast_lap_id" = "5_place_id") FROM sprint_results WHERE "5_place_id" IS NOT NULL AND "5_place_id" > 0 +UNION ALL +SELECT id, "6_place_id", 1, 6, ("fast_lap_id" = "6_place_id") FROM sprint_results WHERE "6_place_id" IS NOT NULL AND "6_place_id" > 0; + +CREATE TABLE qualifying_result_entries ( + qualifying_results_id INTEGER NOT NULL REFERENCES qualifying_results(id) ON DELETE CASCADE, + pilot_id INTEGER NOT NULL REFERENCES pilots(id) ON DELETE CASCADE, + season_id INTEGER NOT NULL REFERENCES seasons(id) ON DELETE CASCADE, + position INTEGER NOT NULL, + PRIMARY KEY (qualifying_results_id, pilot_id, season_id) +); + +-- 4. Migrate qualifying_results data +INSERT INTO qualifying_result_entries (qualifying_results_id, pilot_id, season_id, position) +SELECT id, "1_place_id", 1, 1 FROM qualifying_results WHERE "1_place_id" IS NOT NULL AND "1_place_id" > 0 +UNION ALL +SELECT id, "2_place_id", 1, 2 FROM qualifying_results WHERE "2_place_id" IS NOT NULL AND "2_place_id" > 0 +UNION ALL +SELECT id, "3_place_id", 1, 3 FROM qualifying_results WHERE "3_place_id" IS NOT NULL AND "3_place_id" > 0 +UNION ALL +SELECT id, "4_place_id", 1, 4 FROM qualifying_results WHERE "4_place_id" IS NOT NULL AND "4_place_id" > 0 +UNION ALL +SELECT id, "5_place_id", 1, 5 FROM qualifying_results WHERE "5_place_id" IS NOT NULL AND "5_place_id" > 0 +UNION ALL +SELECT id, "6_place_id", 1, 6 FROM qualifying_results WHERE "6_place_id" IS NOT NULL AND "6_place_id" > 0; + +-- 1. Create free_practice_result_entries (senza fast_lap) +CREATE TABLE free_practice_result_entries ( + free_practice_results_id INTEGER NOT NULL REFERENCES free_practice_results(id) ON DELETE CASCADE, + pilot_id INTEGER NOT NULL REFERENCES pilots(id) ON DELETE CASCADE, + season_id INTEGER NOT NULL REFERENCES seasons(id) ON DELETE CASCADE, + position INTEGER NOT NULL, -- 1 per il migliore, ..., 0 per DNF + PRIMARY KEY (free_practice_results_id, pilot_id, season_id) +); + +-- 2. Migrate free_practice_results data +INSERT INTO free_practice_result_entries (free_practice_results_id, pilot_id, season_id, position) +SELECT id, "1_place_id", 1, 1 FROM free_practice_results WHERE "1_place_id" IS NOT NULL AND "1_place_id" > 0 +UNION ALL +SELECT id, "2_place_id", 1, 2 FROM free_practice_results WHERE "2_place_id" IS NOT NULL AND "2_place_id" > 0 +UNION ALL +SELECT id, "3_place_id", 1, 3 FROM free_practice_results WHERE "3_place_id" IS NOT NULL AND "3_place_id" > 0 +UNION ALL +SELECT id, "4_place_id", 1, 4 FROM free_practice_results WHERE "4_place_id" IS NOT NULL AND "4_place_id" > 0 +UNION ALL +SELECT id, "5_place_id", 1, 5 FROM free_practice_results WHERE "5_place_id" IS NOT NULL AND "5_place_id" > 0 +UNION ALL +SELECT id, "6_place_id", 1, 6 FROM free_practice_results WHERE "6_place_id" IS NOT NULL AND "6_place_id" > 0; + + +-- 1. Free practice result entries +ALTER TABLE free_practice_result_entries +DROP CONSTRAINT IF EXISTS free_practice_result_entries_free_practice_results_id_fkey; +ALTER TABLE free_practice_result_entries +ADD CONSTRAINT free_practice_result_entries_free_practice_results_id_fkey +FOREIGN KEY (free_practice_results_id) +REFERENCES gran_prix(id) +ON UPDATE CASCADE +ON DELETE CASCADE; + +-- 2. Qualifying result entries +ALTER TABLE qualifying_result_entries +DROP CONSTRAINT IF EXISTS qualifying_result_entries_qualifying_results_id_fkey; +ALTER TABLE qualifying_result_entries +ADD CONSTRAINT qualifying_result_entries_qualifying_results_id_fkey +FOREIGN KEY (qualifying_results_id) +REFERENCES gran_prix(id) +ON UPDATE CASCADE +ON DELETE CASCADE; + +-- 3. Race result entries +ALTER TABLE race_result_entries +DROP CONSTRAINT IF EXISTS race_result_entries_race_results_id_fkey; +ALTER TABLE race_result_entries +ADD CONSTRAINT race_result_entries_race_results_id_fkey +FOREIGN KEY (race_results_id) +REFERENCES gran_prix(id) +ON UPDATE CASCADE +ON DELETE CASCADE; + +-- 4. Sprint result entries +ALTER TABLE sprint_result_entries +DROP CONSTRAINT IF EXISTS sprint_result_entries_sprint_results_id_fkey; +ALTER TABLE sprint_result_entries +ADD CONSTRAINT sprint_result_entries_sprint_results_id_fkey +FOREIGN KEY (sprint_results_id) +REFERENCES gran_prix(id) +ON UPDATE CASCADE +ON DELETE CASCADE; \ No newline at end of file diff --git a/server/scripts/populate_db_gp_results.txt b/server/scripts/populate_db_gp_results.txt new file mode 100644 index 000000000..ca0e8f696 --- /dev/null +++ b/server/scripts/populate_db_gp_results.txt @@ -0,0 +1,11 @@ +INSERT INTO "race_results" ("id", "1_place_id", "2_place_id", "3_place_id", "4_place_id", "5_place_id", "6_place_id", "session_type_id", "fast_lap_id", "dnf") VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ''); +INSERT INTO "race_results" ("id", "1_place_id", "2_place_id", "3_place_id", "4_place_id", "5_place_id", "6_place_id", "session_type_id", "fast_lap_id", "dnf") VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ''); + +INSERT INTO "qualifying_results" ("id", "1_place_id", "2_place_id", "3_place_id", "4_place_id", "5_place_id", "6_place_id", "session_type_id") VALUES (?, ?, ?, ?, ?, ?, ?, 2); +INSERT INTO "qualifying_results" ("id", "1_place_id", "2_place_id", "3_place_id", "4_place_id", "5_place_id", "6_place_id", "session_type_id") VALUES (?, ?, ?, ?, ?, ?, ?, 2); + +INSERT INTO "free_practice_results" ("id", "1_place_id", "2_place_id", "3_place_id", "4_place_id", "5_place_id", "6_place_id", "session_type_id") VALUES (?, ?, ?, ?, ?, ?, ?, 3); +INSERT INTO "free_practice_results" ("id", "1_place_id", "2_place_id", "3_place_id", "4_place_id", "5_place_id", "6_place_id", "session_type_id") VALUES (?, ?, ?, ?, ?, ?, ?, 3); + +UPDATE "gran_prix" SET "race_results_id"=?, "qualifying_results_id"=?, "free_practice_results_id"=? WHERE id=?; +UPDATE "gran_prix" SET "race_results_id"=?, "qualifying_results_id"=?, "free_practice_results_id"=? WHERE id=?; \ No newline at end of file diff --git a/server/scripts/view.sql b/server/scripts/view.sql new file mode 100644 index 000000000..3950ddf2b --- /dev/null +++ b/server/scripts/view.sql @@ -0,0 +1,412 @@ +-- public.driver_grand_prix_points source +--//TODO fix drivers not showing when they have 0 points and no results are submitted for the session +CREATE OR REPLACE VIEW public.driver_grand_prix_points +AS WITH session_points AS ( + SELECT gp.id AS grand_prix_id, + gp.date AS grand_prix_date, + t.name AS track_name, + s.description AS season_description, + s.id AS season_id, + p.id AS pilot_id, + (p.name || ' '::text) || p.surname AS pilot_name, + p.username AS pilot_username, + 'Race'::text AS session_type, + CASE + WHEN rre."position" = 1 AND s.id = 1 THEN ( SELECT session_type."1_points" + FROM session_type + WHERE session_type.name = 'race'::text AND session_type.season = 1) + WHEN rre."position" = 2 AND s.id = 1 THEN ( SELECT session_type."2_points" + FROM session_type + WHERE session_type.name = 'race'::text AND session_type.season = 1) + WHEN rre."position" = 3 AND s.id = 1 THEN ( SELECT session_type."3_points" + FROM session_type + WHERE session_type.name = 'race'::text AND session_type.season = 1) + WHEN rre."position" = 4 AND s.id = 1 THEN ( SELECT session_type."4_points" + FROM session_type + WHERE session_type.name = 'race'::text AND session_type.season = 1) + WHEN rre."position" = 5 AND s.id = 1 THEN ( SELECT session_type."5_points" + FROM session_type + WHERE session_type.name = 'race'::text AND session_type.season = 1) + WHEN rre."position" = 6 AND s.id = 1 THEN ( SELECT session_type."6_points" + FROM session_type + WHERE session_type.name = 'race'::text AND session_type.season = 2) + WHEN rre."position" = 1 AND s.id = 2 THEN ( SELECT session_type."1_points" + FROM session_type + WHERE session_type.name = 'race'::text AND session_type.season = 2) + WHEN rre."position" = 2 AND s.id = 2 THEN ( SELECT session_type."2_points" + FROM session_type + WHERE session_type.name = 'race'::text AND session_type.season = 2) + WHEN rre."position" = 3 AND s.id = 2 THEN ( SELECT session_type."3_points" + FROM session_type + WHERE session_type.name = 'race'::text AND session_type.season = 2) + WHEN rre."position" = 4 AND s.id = 2 THEN ( SELECT session_type."4_points" + FROM session_type + WHERE session_type.name = 'race'::text AND session_type.season = 2) + WHEN rre."position" = 5 AND s.id = 2 THEN ( SELECT session_type."5_points" + FROM session_type + WHERE session_type.name = 'race'::text AND session_type.season = 2) + WHEN rre."position" = 6 AND s.id = 2 THEN ( SELECT session_type."6_points" + FROM session_type + WHERE session_type.name = 'race'::text AND session_type.season = 2) + WHEN rre."position" = 7 AND s.id = 2 THEN ( SELECT session_type."7_points" + FROM session_type + WHERE session_type.name = 'race'::text AND session_type.season = 2) + WHEN rre."position" = 8 AND s.id = 2 THEN ( SELECT session_type."8_points" + FROM session_type + WHERE session_type.name = 'race'::text AND session_type.season = 2) + ELSE 0 + END AS position_points, + CASE + WHEN rre.fast_lap = true AND s.id = 1 THEN ( SELECT session_type.fast_lap_points + FROM session_type + WHERE session_type.name = 'race'::text AND session_type.season = 1) + WHEN rre.fast_lap = true AND s.id = 2 THEN ( SELECT session_type.fast_lap_points + FROM session_type + WHERE session_type.name = 'race'::text AND session_type.season = 2) + ELSE 0 + END AS fast_lap_points + FROM gran_prix gp + JOIN race_result_entries rre ON gp.race_results_id = rre.race_results_id + JOIN drivers p ON rre.pilot_id = p.id + JOIN tracks t ON gp.track_id = t.id + JOIN seasons s ON gp.season_id = s.id + WHERE gp.race_results_id IS NOT NULL + UNION ALL + SELECT gp.id AS grand_prix_id, + gp.date AS grand_prix_date, + t.name AS track_name, + s.description AS season_description, + s.id AS season_id, + p.id AS pilot_id, + (p.name || ' '::text) || p.surname AS pilot_name, + p.username AS pilot_username, + 'Sprint'::text AS session_type, + CASE + WHEN sre."position" = 1 AND s.id = 1 THEN ( SELECT session_type."1_points" + FROM session_type + WHERE session_type.name = 'sprint'::text AND session_type.season = 1) + WHEN sre."position" = 2 AND s.id = 1 THEN ( SELECT session_type."2_points" + FROM session_type + WHERE session_type.name = 'sprint'::text AND session_type.season = 1) + WHEN sre."position" = 3 AND s.id = 1 THEN ( SELECT session_type."3_points" + FROM session_type + WHERE session_type.name = 'sprint'::text AND session_type.season = 1) + WHEN sre."position" = 4 AND s.id = 1 THEN ( SELECT session_type."4_points" + FROM session_type + WHERE session_type.name = 'sprint'::text AND session_type.season = 1) + WHEN sre."position" = 5 AND s.id = 1 THEN ( SELECT session_type."5_points" + FROM session_type + WHERE session_type.name = 'sprint'::text AND session_type.season = 1) + WHEN sre."position" = 6 AND s.id = 1 THEN ( SELECT session_type."6_points" + FROM session_type + WHERE session_type.name = 'sprint'::text AND session_type.season = 1) + WHEN sre."position" = 1 AND s.id = 2 THEN ( SELECT session_type."1_points" + FROM session_type + WHERE session_type.name = 'sprint'::text AND session_type.season = 2) + WHEN sre."position" = 2 AND s.id = 2 THEN ( SELECT session_type."2_points" + FROM session_type + WHERE session_type.name = 'sprint'::text AND session_type.season = 2) + WHEN sre."position" = 3 AND s.id = 2 THEN ( SELECT session_type."3_points" + FROM session_type + WHERE session_type.name = 'sprint'::text AND session_type.season = 2) + WHEN sre."position" = 4 AND s.id = 2 THEN ( SELECT session_type."4_points" + FROM session_type + WHERE session_type.name = 'sprint'::text AND session_type.season = 2) + WHEN sre."position" = 5 AND s.id = 2 THEN ( SELECT session_type."5_points" + FROM session_type + WHERE session_type.name = 'sprint'::text AND session_type.season = 2) + WHEN sre."position" = 6 AND s.id = 2 THEN ( SELECT session_type."6_points" + FROM session_type + WHERE session_type.name = 'sprint'::text AND session_type.season = 2) + WHEN sre."position" = 7 AND s.id = 2 THEN ( SELECT session_type."7_points" + FROM session_type + WHERE session_type.name = 'sprint'::text AND session_type.season = 2) + WHEN sre."position" = 8 AND s.id = 2 THEN ( SELECT session_type."8_points" + FROM session_type + WHERE session_type.name = 'sprint'::text AND session_type.season = 2) + ELSE 0 + END AS position_points, + CASE + WHEN sre.fast_lap = true AND s.id = 1 THEN ( SELECT session_type.fast_lap_points + FROM session_type + WHERE session_type.name = 'sprint'::text AND session_type.season = 1) + WHEN sre.fast_lap = true AND s.id = 2 THEN ( SELECT session_type.fast_lap_points + FROM session_type + WHERE session_type.name = 'sprint'::text AND session_type.season = 2) + ELSE 0 + END AS fast_lap_points + FROM gran_prix gp + JOIN sprint_result_entries sre ON gp.sprint_results_id = sre.sprint_results_id + JOIN drivers p ON sre.pilot_id = p.id + JOIN tracks t ON gp.track_id = t.id + JOIN seasons s ON gp.season_id = s.id + WHERE gp.sprint_results_id IS NOT NULL AND gp.has_sprint = 1 + UNION ALL + SELECT gp.id AS grand_prix_id, + gp.date AS grand_prix_date, + t.name AS track_name, + s.description AS season_description, + s.id AS season_id, + p.id AS pilot_id, + (p.name || ' '::text) || p.surname AS pilot_name, + p.username AS pilot_username, + 'Qualifying'::text AS session_type, + CASE + WHEN qre."position" = 1 AND s.id = 1 THEN ( SELECT session_type."1_points" + FROM session_type + WHERE session_type.name = 'qualifying'::text AND session_type.season = 1) + WHEN qre."position" = 2 AND s.id = 1 THEN ( SELECT session_type."2_points" + FROM session_type + WHERE session_type.name = 'qualifying'::text AND session_type.season = 1) + WHEN qre."position" = 3 AND s.id = 1 THEN ( SELECT session_type."3_points" + FROM session_type + WHERE session_type.name = 'qualifying'::text AND session_type.season = 1) + WHEN qre."position" = 4 AND s.id = 1 THEN ( SELECT session_type."4_points" + FROM session_type + WHERE session_type.name = 'qualifying'::text AND session_type.season = 1) + WHEN qre."position" = 5 AND s.id = 1 THEN ( SELECT session_type."5_points" + FROM session_type + WHERE session_type.name = 'qualifying'::text AND session_type.season = 1) + WHEN qre."position" = 6 AND s.id = 1 THEN ( SELECT session_type."6_points" + FROM session_type + WHERE session_type.name = 'qualifying'::text AND session_type.season = 1) + WHEN qre."position" = 1 AND s.id = 2 THEN ( SELECT session_type."1_points" + FROM session_type + WHERE session_type.name = 'qualifying'::text AND session_type.season = 2) + WHEN qre."position" = 2 AND s.id = 2 THEN ( SELECT session_type."2_points" + FROM session_type + WHERE session_type.name = 'qualifying'::text AND session_type.season = 2) + WHEN qre."position" = 3 AND s.id = 2 THEN ( SELECT session_type."3_points" + FROM session_type + WHERE session_type.name = 'qualifying'::text AND session_type.season = 2) + WHEN qre."position" = 4 AND s.id = 2 THEN ( SELECT session_type."4_points" + FROM session_type + WHERE session_type.name = 'qualifying'::text AND session_type.season = 2) + WHEN qre."position" = 5 AND s.id = 2 THEN ( SELECT session_type."5_points" + FROM session_type + WHERE session_type.name = 'qualifying'::text AND session_type.season = 2) + WHEN qre."position" = 6 AND s.id = 2 THEN ( SELECT session_type."6_points" + FROM session_type + WHERE session_type.name = 'qualifying'::text AND session_type.season = 2) + WHEN qre."position" = 7 AND s.id = 2 THEN ( SELECT session_type."7_points" + FROM session_type + WHERE session_type.name = 'qualifying'::text AND session_type.season = 2) + WHEN qre."position" = 8 AND s.id = 2 THEN ( SELECT session_type."8_points" + FROM session_type + WHERE session_type.name = 'qualifying'::text AND session_type.season = 2) + ELSE 0 + END AS position_points, + 0 AS fast_lap_points + FROM gran_prix gp + JOIN qualifying_result_entries qre ON gp.qualifying_results_id = qre.qualifying_results_id + JOIN drivers p ON qre.pilot_id = p.id + JOIN tracks t ON gp.track_id = t.id + JOIN seasons s ON gp.season_id = s.id + WHERE gp.qualifying_results_id IS NOT NULL + UNION ALL + SELECT gp.id AS grand_prix_id, + gp.date AS grand_prix_date, + t.name AS track_name, + s.description AS season_description, + s.id AS season_id, + p.id AS pilot_id, + (p.name || ' '::text) || p.surname AS pilot_name, + p.username AS pilot_username, + 'Free Practice'::text AS session_type, + CASE + WHEN fpre."position" = 1 AND s.id = 1 THEN ( SELECT session_type."1_points" + FROM session_type + WHERE session_type.name = 'free-practice'::text AND session_type.season = 1) + WHEN fpre."position" = 2 AND s.id = 1 THEN ( SELECT session_type."2_points" + FROM session_type + WHERE session_type.name = 'free-practice'::text AND session_type.season = 1) + WHEN fpre."position" = 3 AND s.id = 1 THEN ( SELECT session_type."3_points" + FROM session_type + WHERE session_type.name = 'free-practice'::text AND session_type.season = 1) + WHEN fpre."position" = 4 AND s.id = 1 THEN ( SELECT session_type."4_points" + FROM session_type + WHERE session_type.name = 'free-practice'::text AND session_type.season = 1) + WHEN fpre."position" = 5 AND s.id = 1 THEN ( SELECT session_type."5_points" + FROM session_type + WHERE session_type.name = 'free-practice'::text AND session_type.season = 1) + WHEN fpre."position" = 6 AND s.id = 1 THEN ( SELECT session_type."6_points" + FROM session_type + WHERE session_type.name = 'free-practice'::text AND session_type.season = 1) + WHEN fpre."position" = 1 AND s.id = 2 THEN ( SELECT session_type."1_points" + FROM session_type + WHERE session_type.name = 'free-practice'::text AND session_type.season = 2) + WHEN fpre."position" = 2 AND s.id = 2 THEN ( SELECT session_type."2_points" + FROM session_type + WHERE session_type.name = 'free-practice'::text AND session_type.season = 2) + WHEN fpre."position" = 3 AND s.id = 2 THEN ( SELECT session_type."3_points" + FROM session_type + WHERE session_type.name = 'free-practice'::text AND session_type.season = 2) + WHEN fpre."position" = 4 AND s.id = 2 THEN ( SELECT session_type."4_points" + FROM session_type + WHERE session_type.name = 'free-practice'::text AND session_type.season = 2) + WHEN fpre."position" = 5 AND s.id = 2 THEN ( SELECT session_type."5_points" + FROM session_type + WHERE session_type.name = 'free-practice'::text AND session_type.season = 2) + WHEN fpre."position" = 6 AND s.id = 2 THEN ( SELECT session_type."6_points" + FROM session_type + WHERE session_type.name = 'free-practice'::text AND session_type.season = 2) + WHEN fpre."position" = 7 AND s.id = 2 THEN ( SELECT session_type."7_points" + FROM session_type + WHERE session_type.name = 'free-practice'::text AND session_type.season = 2) + WHEN fpre."position" = 8 AND s.id = 2 THEN ( SELECT session_type."8_points" + FROM session_type + WHERE session_type.name = 'free-practice'::text AND session_type.season = 2) + ELSE 0 + END AS position_points, + 0 AS fast_lap_points + FROM gran_prix gp + JOIN free_practice_result_entries fpre ON gp.free_practice_results_id = fpre.free_practice_results_id + JOIN drivers p ON fpre.pilot_id = p.id + JOIN tracks t ON gp.track_id = t.id + JOIN seasons s ON gp.season_id = s.id + WHERE gp.free_practice_results_id IS NOT NULL + UNION ALL + SELECT gp.id AS grand_prix_id, + gp.date AS grand_prix_date, + t.name AS track_name, + s.description AS season_description, + s.id AS season_id, + p.id AS pilot_id, + (p.name || ' '::text) || p.surname AS pilot_name, + p.username AS pilot_username, + 'Full Race'::text AS session_type, + CASE + WHEN rre."position" = 1 AND s.id = 1 THEN ( SELECT session_type."1_points" + FROM session_type + WHERE session_type.name = 'full-race'::text AND session_type.season = 1) + WHEN rre."position" = 2 AND s.id = 1 THEN ( SELECT session_type."2_points" + FROM session_type + WHERE session_type.name = 'full-race'::text AND session_type.season = 1) + WHEN rre."position" = 3 AND s.id = 1 THEN ( SELECT session_type."3_points" + FROM session_type + WHERE session_type.name = 'full-race'::text AND session_type.season = 1) + WHEN rre."position" = 4 AND s.id = 1 THEN ( SELECT session_type."4_points" + FROM session_type + WHERE session_type.name = 'full-race'::text AND session_type.season = 1) + WHEN rre."position" = 5 AND s.id = 1 THEN ( SELECT session_type."5_points" + FROM session_type + WHERE session_type.name = 'full-race'::text AND session_type.season = 1) + WHEN rre."position" = 6 AND s.id = 1 THEN ( SELECT session_type."6_points" + FROM session_type + WHERE session_type.name = 'full-race'::text AND session_type.season = 1) + WHEN rre."position" = 1 AND s.id = 2 THEN ( SELECT session_type."1_points" + FROM session_type + WHERE session_type.name = 'full-race'::text AND session_type.season = 2) + WHEN rre."position" = 2 AND s.id = 2 THEN ( SELECT session_type."2_points" + FROM session_type + WHERE session_type.name = 'full-race'::text AND session_type.season = 2) + WHEN rre."position" = 3 AND s.id = 2 THEN ( SELECT session_type."3_points" + FROM session_type + WHERE session_type.name = 'full-race'::text AND session_type.season = 2) + WHEN rre."position" = 4 AND s.id = 2 THEN ( SELECT session_type."4_points" + FROM session_type + WHERE session_type.name = 'full-race'::text AND session_type.season = 2) + WHEN rre."position" = 5 AND s.id = 2 THEN ( SELECT session_type."5_points" + FROM session_type + WHERE session_type.name = 'full-race'::text AND session_type.season = 2) + WHEN rre."position" = 6 AND s.id = 2 THEN ( SELECT session_type."6_points" + FROM session_type + WHERE session_type.name = 'full-race'::text AND session_type.season = 2) + WHEN rre."position" = 7 AND s.id = 2 THEN ( SELECT session_type."7_points" + FROM session_type + WHERE session_type.name = 'full-race'::text AND session_type.season = 2) + WHEN rre."position" = 8 AND s.id = 2 THEN ( SELECT session_type."8_points" + FROM session_type + WHERE session_type.name = 'full-race'::text AND session_type.season = 2) + ELSE 0 + END AS position_points, + CASE + WHEN rre.fast_lap = true AND s.id = 1 THEN ( SELECT session_type.fast_lap_points + FROM session_type + WHERE session_type.name = 'full-race'::text AND session_type.season = 1) + WHEN rre.fast_lap = true AND s.id = 2 THEN ( SELECT session_type.fast_lap_points + FROM session_type + WHERE session_type.name = 'full-race'::text AND session_type.season = 2) + ELSE 0 + END AS fast_lap_points + FROM gran_prix gp + JOIN full_race_result_entries rre ON gp.full_race_results_id = rre.race_results_id + JOIN drivers p ON rre.pilot_id = p.id + JOIN tracks t ON gp.track_id = t.id + JOIN seasons s ON gp.season_id = s.id + WHERE gp.full_race_results_id IS NOT NULL AND gp.has_x2 = 1 + ) + SELECT grand_prix_id, + grand_prix_date, + track_name, + season_description, + season_id, + pilot_id, + pilot_name, + pilot_username, + sum(position_points) AS position_points, + sum(fast_lap_points) AS fast_lap_points, + sum(position_points + fast_lap_points) AS total_points, + string_agg((session_type || ': '::text) || ((position_points + fast_lap_points)::text), ', '::text ORDER BY session_type) AS points_breakdown + FROM session_points + GROUP BY grand_prix_id, grand_prix_date, track_name, season_description, season_id, pilot_id, pilot_name, pilot_username + ORDER BY grand_prix_date DESC, (sum(position_points + fast_lap_points)) DESC; +-- public.season_driver_leaderboard source + +CREATE OR REPLACE VIEW public.season_driver_leaderboard +AS SELECT season_id, + pilot_id, + pilot_username, + pilot_name, + sum(total_points) AS total_points + FROM driver_grand_prix_points + GROUP BY season_id, pilot_id, pilot_name, pilot_username + ORDER BY season_id DESC, (sum(total_points)) DESC; + + + +-- public.constructor_grand_prix_points source + +CREATE OR REPLACE VIEW public.constructor_grand_prix_points +AS SELECT c.id AS constructor_id, + c.name AS constructor_name, + dgp.grand_prix_id, + dgp.track_id, + dgp.grand_prix_date, + dgp.track_name, + dgp.season_id, + dgp.season_description, + c.driver_id_1, + c.driver_id_2, + COALESCE(d1_points.total_points, 0::bigint) AS driver_1_points, + COALESCE(d2_points.total_points, 0::bigint) AS driver_2_points, + COALESCE(d1_points.total_points, 0::bigint) + COALESCE(d2_points.total_points, 0::bigint) AS constructor_points + FROM constructors c + CROSS JOIN ( SELECT DISTINCT driver_grand_prix_points.grand_prix_id, + driver_grand_prix_points.grand_prix_date, + driver_grand_prix_points.track_name, + driver_grand_prix_points.track_id, + driver_grand_prix_points.season_id, + driver_grand_prix_points.season_description + FROM driver_grand_prix_points) dgp + LEFT JOIN driver_grand_prix_points d1_points ON c.driver_id_1 = d1_points.pilot_id AND dgp.grand_prix_id = d1_points.grand_prix_id + LEFT JOIN driver_grand_prix_points d2_points ON c.driver_id_2 = d2_points.pilot_id AND dgp.grand_prix_id = d2_points.grand_prix_id + WHERE c.driver_id_1 IS NOT NULL AND c.driver_id_2 IS NOT NULL + ORDER BY dgp.grand_prix_date DESC, (COALESCE(d1_points.total_points, 0::bigint) + COALESCE(d2_points.total_points, 0::bigint)) DESC; + +-- public.season_constructor_leaderboard source +CREATE OR REPLACE VIEW public.season_constructor_leaderboard +AS SELECT c.id AS constructor_id, + c.name AS constructor_name, + c.color AS constructor_color, + d1.pilot_id AS driver_1_id, + d1.pilot_username AS driver_1_username, + d1.total_points AS driver_1_tot_points, + d2.pilot_id AS driver_2_id, + d2.pilot_username AS driver_2_username, + d2.total_points AS driver_2_tot_points, + d1.total_points + d2.total_points AS constructor_tot_points + FROM constructors c + LEFT JOIN season_driver_leaderboard d1 ON c.driver_id_1 = d1.pilot_id + LEFT JOIN season_driver_leaderboard d2 ON c.driver_id_2 = d2.pilot_id + ORDER BY (d1.total_points + d2.total_points) DESC; \ No newline at end of file diff --git a/server/src/all_queries.sql b/server/src/all_queries.sql new file mode 100644 index 000000000..43d4663b5 --- /dev/null +++ b/server/src/all_queries.sql @@ -0,0 +1,1181 @@ +/*******************************************************/ +/* ALL DRIVERS POINTS: RACE, QUALIFYING, FREE-PRACTICE */ +/*******************************************************/ +WITH all_race_points AS +( + SELECT + race_results.id, + race_results."1_place_id" AS driver_id, + (SELECT "1_points" FROM session_type WHERE session_type.id = 1) as race_point + FROM race_results + + UNION ALL + + SELECT + race_results.id, + race_results."2_place_id" AS driver_id, + (SELECT "2_points" FROM session_type WHERE session_type.id = 1) as race_point + FROM race_results + + UNION ALL + + SELECT + race_results.id, + race_results."3_place_id" AS driver_id, + (SELECT "3_points" FROM session_type WHERE session_type.id = 1) as race_point + FROM race_results + + UNION ALL + + SELECT + race_results.id, + race_results."4_place_id" AS driver_id, + (SELECT "4_points" FROM session_type WHERE session_type.id = 1) as race_point + FROM race_results + + UNION ALL + + SELECT + race_results.id, + race_results."5_place_id" AS driver_id, + (SELECT "5_points" FROM session_type WHERE session_type.id = 1) as race_point + FROM race_results + + UNION ALL + + SELECT + race_results.id, + race_results."6_place_id" AS driver_id, + (SELECT "6_points" FROM session_type WHERE session_type.id = 1) as race_point + FROM race_results + + UNION ALL + + SELECT + race_results.id, + race_results."fast_lap_id" AS driver_id, + (SELECT "fast_lap_points" FROM session_type WHERE session_type.id = 1) as race_point + FROM race_results +), +all_sprint_points AS +( + SELECT + sprint_results.id, + sprint_results."1_place_id" AS driver_id, + (SELECT "1_points" FROM session_type WHERE session_type.id = 4) as sprint_point + FROM sprint_results + + UNION ALL + + SELECT + sprint_results.id, + sprint_results."2_place_id" AS driver_id, + (SELECT "2_points" FROM session_type WHERE session_type.id = 4) as sprint_point + FROM sprint_results + + UNION ALL + + SELECT + sprint_results.id, + sprint_results."3_place_id" AS driver_id, + (SELECT "3_points" FROM session_type WHERE session_type.id = 4) as sprint_point + FROM sprint_results + + UNION ALL + + SELECT + sprint_results.id, + sprint_results."4_place_id" AS driver_id, + (SELECT "4_points" FROM session_type WHERE session_type.id = 4) as sprint_point + FROM sprint_results + + UNION ALL + + SELECT + sprint_results.id, + sprint_results."5_place_id" AS driver_id, + (SELECT "5_points" FROM session_type WHERE session_type.id = 4) as sprint_point + FROM sprint_results + + UNION ALL + + SELECT + sprint_results.id, + sprint_results."6_place_id" AS driver_id, + (SELECT "6_points" FROM session_type WHERE session_type.id = 4) as sprint_point + FROM sprint_results + + UNION ALL + + SELECT + sprint_results.id, + sprint_results."fast_lap_id" AS driver_id, + (SELECT "fast_lap_points" FROM session_type WHERE session_type.id = 4) as sprint_point + FROM sprint_results +), +all_qualifying_points AS +( + SELECT + qualifying_results.id, + qualifying_results."1_place_id" AS driver_id, + (SELECT "1_points" FROM session_type WHERE session_type.id = 2) as qualifying_point + FROM qualifying_results + + UNION ALL + + SELECT + qualifying_results.id, + qualifying_results."2_place_id" AS driver_id, + (SELECT "2_points" FROM session_type WHERE session_type.id = 2) as qualifying_point + FROM qualifying_results + + UNION ALL + + SELECT + qualifying_results.id, + qualifying_results."3_place_id" AS driver_id, + (SELECT "3_points" FROM session_type WHERE session_type.id = 2) as qualifying_point + FROM qualifying_results + + UNION ALL + + SELECT + qualifying_results.id, + qualifying_results."4_place_id" AS driver_id, + (SELECT "4_points" FROM session_type WHERE session_type.id = 2) as qualifying_point + FROM qualifying_results + + UNION ALL + + SELECT + qualifying_results.id, + qualifying_results."5_place_id" AS driver_id, + (SELECT "5_points" FROM session_type WHERE session_type.id = 2) as qualifying_point + FROM qualifying_results + + UNION ALL + + SELECT + qualifying_results.id, + qualifying_results."6_place_id" AS driver_id, + (SELECT "6_points" FROM session_type WHERE session_type.id = 2) as qualifying_point + FROM qualifying_results +), +all_free_practice_points AS +( + SELECT + free_practice_results.id, + free_practice_results."1_place_id" AS driver_id, + (SELECT "1_points" FROM session_type WHERE session_type.id = 3) as free_practice_point + FROM free_practice_results + + UNION ALL + + SELECT + free_practice_results.id, + free_practice_results."2_place_id" AS driver_id, + (SELECT "2_points" FROM session_type WHERE session_type.id = 3) as free_practice_point + FROM free_practice_results + + UNION ALL + + SELECT + free_practice_results.id, + free_practice_results."3_place_id" AS driver_id, + (SELECT "3_points" FROM session_type WHERE session_type.id = 3) as free_practice_point + FROM free_practice_results + + UNION ALL + + SELECT + free_practice_results.id, + free_practice_results."4_place_id" AS driver_id, + (SELECT "4_points" FROM session_type WHERE session_type.id = 3) as free_practice_point + FROM free_practice_results + + UNION ALL + + SELECT + free_practice_results.id, + free_practice_results."5_place_id" AS driver_id, + (SELECT "5_points" FROM session_type WHERE session_type.id = 3) as free_practice_point + FROM free_practice_results + + UNION ALL + + SELECT + free_practice_results.id, + free_practice_results."6_place_id" AS driver_id, + (SELECT "6_points" FROM session_type WHERE session_type.id = 3) as free_practice_point + FROM free_practice_results +), +all_drivers +AS +( + SELECT drivers.id AS driver_id, drivers.username AS driver_username, drivers.name AS driver_name, drivers.surname AS driver_surname, drivers.description as driver_description, + drivers.consistency_pt AS driver_consistency_pt, drivers.fast_lap_pt AS driver_fast_lap_pt, drivers.dangerous_pt AS drivers_dangerous_pt, drivers.ingenuity_pt AS driver_ingenuity_pt, + drivers.strategy_pt AS driver_strategy_pt, drivers.color AS driver_color, inner_table.pilot_name AS pilot_name, inner_table.pilot_surname AS pilot_surname, inner_table.car_name AS car_name, + inner_table.car_overall_score AS car_overall_score + FROM drivers + INNER JOIN + ( + SELECT pilots.id AS pilot_id, pilots.name AS pilot_name, pilots.surname AS pilot_surname, cars.name AS car_name, cars.overall_score AS car_overall_score + FROM pilots + INNER JOIN cars + ON pilots.car_id = cars.id + ) AS inner_table + ON drivers.pilot_id = inner_table.pilot_id +) + +SELECT *, (inner_table.total_free_practice_points + inner_table.total_qualifying_points + inner_table.total_sprint_points + inner_table.total_race_points) AS total_points +FROM all_drivers +INNER JOIN +( + SELECT all_free_practice_points.driver_id, SUM(all_free_practice_points.free_practice_point) AS total_free_practice_points, COALESCE(inner_table_1.total_qualifying_points,0) AS total_qualifying_points, inner_table_1.total_sprint_points, inner_table_1.total_race_points + FROM all_free_practice_points + LEFT JOIN + ( + SELECT all_qualifying_points.driver_id AS driver_id, SUM(all_qualifying_points.qualifying_point) AS total_qualifying_points, COALESCE(inner_table_2.total_sprint_points,0) AS total_sprint_points, COALESCE(inner_table_2.total_race_points,0) AS total_race_points + FROM all_qualifying_points + LEFT JOIN + ( + SELECT all_race_points.driver_id AS driver_id, SUM(all_race_points.race_point) AS total_race_points, COALESCE(inner_table_3.total_sprint_points,0) AS total_sprint_points + FROM all_race_points + LEFT JOIN + ( + SELECT all_sprint_points.driver_id AS driver_id, SUM(all_sprint_points.sprint_point) AS total_sprint_points + FROM all_sprint_points + GROUP BY all_sprint_points.driver_id + ) AS inner_table_3 + ON inner_table_3.driver_id = all_race_points.driver_id + GROUP BY all_race_points.driver_id, inner_table_3.total_sprint_points + ) AS inner_table_2 + ON inner_table_2.driver_id = all_qualifying_points.driver_id + GROUP BY all_qualifying_points.driver_id, inner_table_2.total_race_points, inner_table_2.total_sprint_points + ) AS inner_table_1 + ON inner_table_1.driver_id = all_free_practice_points.driver_id + GROUP BY all_free_practice_points.driver_id, inner_table_1.total_race_points, inner_table_1.total_sprint_points, inner_table_1.total_qualifying_points +) AS inner_table +ON all_drivers.driver_id = inner_table.driver_id +ORDER BY total_points DESC + + + + + + + +/******************/ +/* ALL GP RESULTS */ +/******************/ +SELECT inner_table_tracks.name AS track_name, gran_prix.date AS gran_prix_date, gran_prix.has_sprint AS gran_prix_has_sprint, inner_table_tracks.country AS track_country, +inner_table_race."1_place_username" AS driver_race_1_place, inner_table_race."2_place_username" AS driver_race_2_place, inner_table_race."3_place_username" AS driver_race_3_place, inner_table_race."4_place_username" AS driver_race_4_place, inner_table_race."5_place_username" AS driver_race_5_place, inner_table_race."6_place_username" AS driver_race_6_place, inner_table_race."fast_lap_username" AS driver_race_fast_lap, inner_table_race.race_dnf AS race_dnf, +inner_table_sprint."1_place_username" AS driver_sprint_1_place, inner_table_sprint."2_place_username" AS driver_sprint_2_place, inner_table_sprint."3_place_username" AS driver_sprint_3_place, inner_table_sprint."4_place_username" AS driver_sprint_4_place, inner_table_sprint."5_place_username" AS driver_sprint_5_place, inner_table_sprint."6_place_username" AS driver_sprint_6_place, inner_table_sprint."fast_lap_username" AS driver_sprint_fast_lap, inner_table_sprint.sprint_dnf AS sprint_dnf, +inner_table_qualifying."1_place_username" AS driver_qualifying_1_place, inner_table_qualifying."2_place_username" AS driver_qualifying_2_place, inner_table_qualifying."3_place_username" AS driver_qualifying_3_place, inner_table_qualifying."4_place_username" AS driver_qualifying_4_place, inner_table_qualifying."5_place_username" AS driver_qualifying_5_place, inner_table_qualifying."6_place_username" AS driver_qualifying_6_place, +inner_table_free_practice."1_place_username" AS driver_free_practice_1_place, inner_table_free_practice."2_place_username" AS driver_free_practice_2_place, inner_table_free_practice."3_place_username" AS driver_free_practice_3_place, inner_table_free_practice."4_place_username" AS driver_free_practice_4_place, inner_table_free_practice."5_place_username" AS driver_free_practice_5_place, inner_table_free_practice."6_place_username" AS driver_free_practice_6_place +FROM gran_prix +LEFT JOIN +( + SELECT first_table.race_id, first_table.race_dnf, first_table."1_place_username", second_table."2_place_username", third_table."3_place_username", fourth_table."4_place_username", fifth_table."5_place_username", sixth_table."6_place_username", fast_lap_table."fast_lap_username" + FROM + ( + SELECT race_results.id AS race_id, race_results.dnf AS race_dnf, drivers.username AS "1_place_username" + FROM race_results + LEFT JOIN drivers + ON race_results."1_place_id" = drivers.id + ) AS first_table + INNER JOIN + ( + SELECT race_results.id AS race_id, drivers.username AS "2_place_username" + FROM race_results + LEFT JOIN drivers + ON race_results."2_place_id" = drivers.id + ) AS second_table + ON first_table.race_id = second_table.race_id + INNER JOIN + ( + SELECT race_results.id AS race_id, drivers.username AS "3_place_username" + FROM race_results + LEFT JOIN drivers + ON race_results."3_place_id" = drivers.id + ) AS third_table + ON first_table.race_id = third_table.race_id + INNER JOIN + ( + SELECT race_results.id AS race_id, drivers.username AS "4_place_username" + FROM race_results + LEFT JOIN drivers + ON race_results."4_place_id" = drivers.id + ) AS fourth_table + ON first_table.race_id = fourth_table.race_id + INNER JOIN + ( + SELECT race_results.id AS race_id, drivers.username AS "5_place_username" + FROM race_results + LEFT JOIN drivers + ON race_results."5_place_id" = drivers.id + ) AS fifth_table + ON first_table.race_id = fifth_table.race_id + INNER JOIN + ( + SELECT race_results.id AS race_id, drivers.username AS "6_place_username" + FROM race_results + LEFT JOIN drivers + ON race_results."6_place_id" = drivers.id + ) AS sixth_table + ON first_table.race_id = sixth_table.race_id + INNER JOIN + ( + SELECT race_results.id AS race_id, drivers.username AS "fast_lap_username" + FROM race_results + LEFT JOIN drivers + ON race_results."fast_lap_id" = drivers.id + ) AS fast_lap_table + ON first_table.race_id = fast_lap_table.race_id +) AS inner_table_race +ON gran_prix.race_results_id = inner_table_race.race_id +LEFT JOIN +( + SELECT first_table.sprint_id, first_table.sprint_dnf, first_table."1_place_username", second_table."2_place_username", third_table."3_place_username", fourth_table."4_place_username", fifth_table."5_place_username", sixth_table."6_place_username", fast_lap_table."fast_lap_username" + FROM + ( + SELECT sprint_results.id AS sprint_id, sprint_results.dnf AS sprint_dnf, drivers.username AS "1_place_username" + FROM sprint_results + LEFT JOIN drivers + ON sprint_results."1_place_id" = drivers.id + ) AS first_table + INNER JOIN + ( + SELECT sprint_results.id AS sprint_id, drivers.username AS "2_place_username" + FROM sprint_results + LEFT JOIN drivers + ON sprint_results."2_place_id" = drivers.id + ) AS second_table + ON first_table.sprint_id = second_table.sprint_id + INNER JOIN + ( + SELECT sprint_results.id AS sprint_id, drivers.username AS "3_place_username" + FROM sprint_results + LEFT JOIN drivers + ON sprint_results."3_place_id" = drivers.id + ) AS third_table + ON first_table.sprint_id = third_table.sprint_id + INNER JOIN + ( + SELECT sprint_results.id AS sprint_id, drivers.username AS "4_place_username" + FROM sprint_results + LEFT JOIN drivers + ON sprint_results."4_place_id" = drivers.id + ) AS fourth_table + ON first_table.sprint_id = fourth_table.sprint_id + INNER JOIN + ( + SELECT sprint_results.id AS sprint_id, drivers.username AS "5_place_username" + FROM sprint_results + LEFT JOIN drivers + ON sprint_results."5_place_id" = drivers.id + ) AS fifth_table + ON first_table.sprint_id = fifth_table.sprint_id + INNER JOIN + ( + SELECT sprint_results.id AS sprint_id, drivers.username AS "6_place_username" + FROM sprint_results + LEFT JOIN drivers + ON sprint_results."6_place_id" = drivers.id + ) AS sixth_table + ON first_table.sprint_id = sixth_table.sprint_id + INNER JOIN + ( + SELECT sprint_results.id AS sprint_id, drivers.username AS "fast_lap_username" + FROM sprint_results + LEFT JOIN drivers + ON sprint_results."fast_lap_id" = drivers.id + ) AS fast_lap_table + ON first_table.sprint_id = fast_lap_table.sprint_id +) AS inner_table_sprint +ON gran_prix.sprint_results_id = inner_table_sprint.sprint_id +LEFT JOIN +( + SELECT first_table.qualifying_id, first_table."1_place_username", second_table."2_place_username", third_table."3_place_username", fourth_table."4_place_username", fifth_table."5_place_username", sixth_table."6_place_username" + FROM + ( + SELECT qualifying_results.id AS qualifying_id, drivers.username AS "1_place_username" + FROM qualifying_results + LEFT JOIN drivers + ON qualifying_results."1_place_id" = drivers.id + ) AS first_table + INNER JOIN + ( + SELECT qualifying_results.id AS qualifying_id, drivers.username AS "2_place_username" + FROM qualifying_results + LEFT JOIN drivers + ON qualifying_results."2_place_id" = drivers.id + ) AS second_table + ON first_table.qualifying_id = second_table.qualifying_id + INNER JOIN + ( + SELECT qualifying_results.id AS qualifying_id, drivers.username AS "3_place_username" + FROM qualifying_results + LEFT JOIN drivers + ON qualifying_results."3_place_id" = drivers.id + ) AS third_table + ON first_table.qualifying_id = third_table.qualifying_id + INNER JOIN + ( + SELECT qualifying_results.id AS qualifying_id, drivers.username AS "4_place_username" + FROM qualifying_results + LEFT JOIN drivers + ON qualifying_results."4_place_id" = drivers.id + ) AS fourth_table + ON first_table.qualifying_id = fourth_table.qualifying_id + INNER JOIN + ( + SELECT qualifying_results.id AS qualifying_id, drivers.username AS "5_place_username" + FROM qualifying_results + LEFT JOIN drivers + ON qualifying_results."5_place_id" = drivers.id + ) AS fifth_table + ON first_table.qualifying_id = fifth_table.qualifying_id + INNER JOIN + ( + SELECT qualifying_results.id AS qualifying_id, drivers.username AS "6_place_username" + FROM qualifying_results + LEFT JOIN drivers + ON qualifying_results."6_place_id" = drivers.id + ) AS sixth_table + ON first_table.qualifying_id = sixth_table.qualifying_id +) AS inner_table_qualifying +ON gran_prix.qualifying_results_id = inner_table_qualifying.qualifying_id +LEFT JOIN +( + SELECT first_table.free_practice_id, first_table."1_place_username", second_table."2_place_username", third_table."3_place_username", fourth_table."4_place_username", fifth_table."5_place_username", sixth_table."6_place_username" + FROM + ( + SELECT free_practice_results.id AS free_practice_id, drivers.username AS "1_place_username" + FROM free_practice_results + LEFT JOIN drivers + ON free_practice_results."1_place_id" = drivers.id + ) AS first_table + INNER JOIN + ( + SELECT free_practice_results.id AS free_practice_id, drivers.username AS "2_place_username" + FROM free_practice_results + LEFT JOIN drivers + ON free_practice_results."2_place_id" = drivers.id + ) AS second_table + ON first_table.free_practice_id = second_table.free_practice_id + INNER JOIN + ( + SELECT free_practice_results.id AS free_practice_id, drivers.username AS "3_place_username" + FROM free_practice_results + LEFT JOIN drivers + ON free_practice_results."3_place_id" = drivers.id + ) AS third_table + ON first_table.free_practice_id = third_table.free_practice_id + INNER JOIN + ( + SELECT free_practice_results.id AS free_practice_id, drivers.username AS "4_place_username" + FROM free_practice_results + LEFT JOIN drivers + ON free_practice_results."4_place_id" = drivers.id + ) AS fourth_table + ON first_table.free_practice_id = fourth_table.free_practice_id + INNER JOIN + ( + SELECT free_practice_results.id AS free_practice_id, drivers.username AS "5_place_username" + FROM free_practice_results + LEFT JOIN drivers + ON free_practice_results."5_place_id" = drivers.id + ) AS fifth_table + ON first_table.free_practice_id = fifth_table.free_practice_id + INNER JOIN + ( + SELECT free_practice_results.id AS free_practice_id, drivers.username AS "6_place_username" + FROM free_practice_results + LEFT JOIN drivers + ON free_practice_results."6_place_id" = drivers.id + ) AS sixth_table + ON first_table.free_practice_id = sixth_table.free_practice_id +) AS inner_table_free_practice +ON gran_prix.free_practice_results_id = inner_table_free_practice.free_practice_id +LEFT JOIN +( + SELECT * + FROM tracks +) AS inner_table_tracks +ON gran_prix.track_id = inner_table_tracks.id +ORDER BY gran_prix.date ASC + + + + + + +/**********************/ +/* CHAMPIONSHIP TREND */ +/**********************/ +WITH all_race_points AS +( + SELECT outer_table.date, outer_table.track_name, outer_table.id, outer_table.driver_id, outer_table.race_point, + drivers.username AS driver_username, drivers.color AS driver_color + FROM drivers + RIGHT JOIN + ( + SELECT gran_prix.date, gran_prix.track_name, inner_table.id, inner_table.driver_id, inner_table.race_point + FROM + ( + SELECT *, track_table.name AS track_name + FROM gran_prix + LEFT JOIN + ( + SELECT * + FROM tracks + ) AS track_table + ON gran_prix.track_id = track_table.id + ) AS gran_prix + RIGHT JOIN + ( + SELECT + race_results.id, + race_results."1_place_id" AS driver_id, + (SELECT "1_points" FROM session_type WHERE session_type.id = 1) as race_point + FROM race_results + + UNION ALL + + SELECT + race_results.id, + race_results."2_place_id" AS driver_id, + (SELECT "2_points" FROM session_type WHERE session_type.id = 1) as race_point + FROM race_results + + UNION ALL + + SELECT + race_results.id, + race_results."3_place_id" AS driver_id, + (SELECT "3_points" FROM session_type WHERE session_type.id = 1) as race_point + FROM race_results + + UNION ALL + + SELECT + race_results.id, + race_results."4_place_id" AS driver_id, + (SELECT "4_points" FROM session_type WHERE session_type.id = 1) as race_point + FROM race_results + + UNION ALL + + SELECT + race_results.id, + race_results."5_place_id" AS driver_id, + (SELECT "5_points" FROM session_type WHERE session_type.id = 1) as race_point + FROM race_results + + UNION ALL + + SELECT + race_results.id, + race_results."6_place_id" AS driver_id, + (SELECT "6_points" FROM session_type WHERE session_type.id = 1) as race_point + FROM race_results + + UNION ALL + + SELECT + race_results.id, + race_results."fast_lap_id" AS driver_id, + (SELECT "fast_lap_points" FROM session_type WHERE session_type.id = 1) as race_point + FROM race_results + ) AS inner_table + ON gran_prix.race_results_id = inner_table.id + ) AS outer_table + ON drivers.id = outer_table.driver_id +), +all_sprint_points AS +( + SELECT outer_table.date, outer_table.track_name, outer_table.id, outer_table.driver_id, outer_table.sprint_point, + drivers.username AS driver_username, drivers.color AS driver_color + FROM drivers + RIGHT JOIN + ( + SELECT gran_prix.date, gran_prix.track_name, inner_table.id, inner_table.driver_id, inner_table.sprint_point + FROM + ( + SELECT *, track_table.name AS track_name + FROM gran_prix + LEFT JOIN + ( + SELECT * + FROM tracks + ) AS track_table + ON gran_prix.track_id = track_table.id + ) AS gran_prix + RIGHT JOIN + ( + SELECT + sprint_results.id, + sprint_results."1_place_id" AS driver_id, + (SELECT "1_points" FROM session_type WHERE session_type.id = 4) as sprint_point + FROM sprint_results + + UNION ALL + + SELECT + sprint_results.id, + sprint_results."2_place_id" AS driver_id, + (SELECT "2_points" FROM session_type WHERE session_type.id = 4) as sprint_point + FROM sprint_results + + UNION ALL + + SELECT + sprint_results.id, + sprint_results."3_place_id" AS driver_id, + (SELECT "3_points" FROM session_type WHERE session_type.id = 4) as sprint_point + FROM sprint_results + + UNION ALL + + SELECT + sprint_results.id, + sprint_results."4_place_id" AS driver_id, + (SELECT "4_points" FROM session_type WHERE session_type.id = 4) as sprint_point + FROM sprint_results + + UNION ALL + + SELECT + sprint_results.id, + sprint_results."5_place_id" AS driver_id, + (SELECT "5_points" FROM session_type WHERE session_type.id = 4) as sprint_point + FROM sprint_results + + UNION ALL + + SELECT + sprint_results.id, + sprint_results."6_place_id" AS driver_id, + (SELECT "6_points" FROM session_type WHERE session_type.id = 4) as sprint_point + FROM sprint_results + + UNION ALL + + SELECT + sprint_results.id, + sprint_results."fast_lap_id" AS driver_id, + (SELECT "fast_lap_points" FROM session_type WHERE session_type.id = 4) as sprint_point + FROM sprint_results + ) AS inner_table + ON gran_prix.sprint_results_id = inner_table.id + ) AS outer_table + ON drivers.id = outer_table.driver_id +), +all_qualifying_points AS +( + SELECT outer_table.date, outer_table.track_name, outer_table.id, outer_table.driver_id, outer_table.qualifying_point, + drivers.username AS driver_username, drivers.color AS driver_color + FROM drivers + RIGHT JOIN + ( + SELECT gran_prix.date, gran_prix.track_name, inner_table.id, inner_table.driver_id, inner_table.qualifying_point + FROM + ( + SELECT *, track_table.name AS track_name + FROM gran_prix + LEFT JOIN + ( + SELECT * + FROM tracks + ) AS track_table + ON gran_prix.track_id = track_table.id + ) AS gran_prix + RIGHT JOIN + ( + SELECT + qualifying_results.id, + qualifying_results."1_place_id" AS driver_id, + (SELECT "1_points" FROM session_type WHERE session_type.id = 2) as qualifying_point + FROM qualifying_results + + UNION ALL + + SELECT + qualifying_results.id, + qualifying_results."2_place_id" AS driver_id, + (SELECT "2_points" FROM session_type WHERE session_type.id = 2) as qualifying_point + FROM qualifying_results + + UNION ALL + + SELECT + qualifying_results.id, + qualifying_results."3_place_id" AS driver_id, + (SELECT "3_points" FROM session_type WHERE session_type.id = 2) as qualifying_point + FROM qualifying_results + + UNION ALL + + SELECT + qualifying_results.id, + qualifying_results."4_place_id" AS driver_id, + (SELECT "4_points" FROM session_type WHERE session_type.id = 2) as qualifying_point + FROM qualifying_results + + UNION ALL + + SELECT + qualifying_results.id, + qualifying_results."5_place_id" AS driver_id, + (SELECT "5_points" FROM session_type WHERE session_type.id = 2) as qualifying_point + FROM qualifying_results + + UNION ALL + + SELECT + qualifying_results.id, + qualifying_results."6_place_id" AS driver_id, + (SELECT "6_points" FROM session_type WHERE session_type.id = 2) as qualifying_point + FROM qualifying_results + ) AS inner_table + ON gran_prix.qualifying_results_id = inner_table.id + ) AS outer_table + ON drivers.id = outer_table.driver_id +), +all_free_practice_points AS +( + SELECT outer_table.date, outer_table.track_name, outer_table.id, outer_table.driver_id, outer_table.free_practice_point, + drivers.username AS driver_username, drivers.color AS driver_color + FROM drivers + RIGHT JOIN + ( + SELECT gran_prix.date, gran_prix.track_name, inner_table.id, inner_table.driver_id, inner_table.free_practice_point + FROM + ( + SELECT *, track_table.name AS track_name + FROM gran_prix + LEFT JOIN + ( + SELECT * + FROM tracks + ) AS track_table + ON gran_prix.track_id = track_table.id + ) AS gran_prix + RIGHT JOIN + ( + SELECT + free_practice_results.id, + free_practice_results."1_place_id" AS driver_id, + (SELECT "1_points" FROM session_type WHERE session_type.id = 3) as free_practice_point + FROM free_practice_results + + UNION ALL + + SELECT + free_practice_results.id, + free_practice_results."2_place_id" AS driver_id, + (SELECT "2_points" FROM session_type WHERE session_type.id = 3) as free_practice_point + FROM free_practice_results + + UNION ALL + + SELECT + free_practice_results.id, + free_practice_results."3_place_id" AS driver_id, + (SELECT "3_points" FROM session_type WHERE session_type.id = 3) as free_practice_point + FROM free_practice_results + + UNION ALL + + SELECT + free_practice_results.id, + free_practice_results."4_place_id" AS driver_id, + (SELECT "4_points" FROM session_type WHERE session_type.id = 3) as free_practice_point + FROM free_practice_results + + UNION ALL + + SELECT + free_practice_results.id, + free_practice_results."5_place_id" AS driver_id, + (SELECT "5_points" FROM session_type WHERE session_type.id = 3) as free_practice_point + FROM free_practice_results + + UNION ALL + + SELECT + free_practice_results.id, + free_practice_results."6_place_id" AS driver_id, + (SELECT "6_points" FROM session_type WHERE session_type.id = 3) as free_practice_point + FROM free_practice_results + ) AS inner_table + ON gran_prix.free_practice_results_id = inner_table.id + ) AS outer_table + ON drivers.id = outer_table.driver_id +) + +SELECT * +FROM +( + SELECT date, track_name, driver_id, driver_username, driver_color, SUM(session_point) OVER (ORDER BY date, track_name) AS cumulative_points + FROM + ( + SELECT date, track_name, driver_id, driver_username, driver_color, race_point AS session_point + FROM all_race_points + + UNION ALL + + SELECT date, track_name, driver_id, driver_username, driver_color, sprint_point AS session_point + FROM all_sprint_points + + UNION ALL + + SELECT date, track_name, driver_id, driver_username, driver_color, qualifying_point AS session_point + FROM all_qualifying_points + + UNION ALL + + SELECT date, track_name, driver_id, driver_username, driver_color, free_practice_point AS session_point + FROM all_free_practice_points + ) + WHERE driver_id = 1 +) +GROUP BY date, track_name, driver_id, driver_username, driver_color, cumulative_points + +UNION ALL + +SELECT * +FROM +( + SELECT date, track_name, driver_id, driver_username, driver_color, SUM(session_point) OVER (ORDER BY date, track_name) AS cumulative_points + FROM + ( + SELECT date, track_name, driver_id, driver_username, driver_color, race_point AS session_point + FROM all_race_points + + UNION ALL + + SELECT date, track_name, driver_id, driver_username, driver_color, sprint_point AS session_point + FROM all_sprint_points + + UNION ALL + + SELECT date, track_name, driver_id, driver_username, driver_color, qualifying_point AS session_point + FROM all_qualifying_points + + UNION ALL + + SELECT date, track_name, driver_id, driver_username, driver_color, free_practice_point AS session_point + FROM all_free_practice_points + ) + WHERE driver_id = 2 +) +GROUP BY date, track_name, driver_id, driver_username, driver_color, cumulative_points + +UNION ALL + +SELECT * +FROM +( + SELECT date, track_name, driver_id, driver_username, driver_color, SUM(session_point) OVER (ORDER BY date, track_name) AS cumulative_points + FROM + ( + SELECT date, track_name, driver_id, driver_username, driver_color, race_point AS session_point + FROM all_race_points + + UNION ALL + + SELECT date, track_name, driver_id, driver_username, driver_color, sprint_point AS session_point + FROM all_sprint_points + + UNION ALL + + SELECT date, track_name, driver_id, driver_username, driver_color, qualifying_point AS session_point + FROM all_qualifying_points + + UNION ALL + + SELECT date, track_name, driver_id, driver_username, driver_color, free_practice_point AS session_point + FROM all_free_practice_points + ) + WHERE driver_id = 3 +) +GROUP BY date, track_name, driver_id, driver_username, driver_color, cumulative_points + +UNION ALL + +SELECT * +FROM +( + SELECT date, track_name, driver_id, driver_username, driver_color, SUM(session_point) OVER (ORDER BY date, track_name) AS cumulative_points + FROM + ( + SELECT date, track_name, driver_id, driver_username, driver_color, race_point AS session_point + FROM all_race_points + + UNION ALL + + SELECT date, track_name, driver_id, driver_username, driver_color, sprint_point AS session_point + FROM all_sprint_points + + UNION ALL + + SELECT date, track_name, driver_id, driver_username, driver_color, qualifying_point AS session_point + FROM all_qualifying_points + + UNION ALL + + SELECT date, track_name, driver_id, driver_username, driver_color, free_practice_point AS session_point + FROM all_free_practice_points + ) + WHERE driver_id = 4 +) +GROUP BY date, track_name, driver_id, driver_username, driver_color, cumulative_points + +UNION ALL + +SELECT * +FROM +( + SELECT date, track_name, driver_id, driver_username, driver_color, SUM(session_point) OVER (ORDER BY date, track_name) AS cumulative_points + FROM + ( + SELECT date, track_name, driver_id, driver_username, driver_color, race_point AS session_point + FROM all_race_points + + UNION ALL + + SELECT date, track_name, driver_id, driver_username, driver_color, sprint_point AS session_point + FROM all_sprint_points + + UNION ALL + + SELECT date, track_name, driver_id, driver_username, driver_color, qualifying_point AS session_point + FROM all_qualifying_points + + UNION ALL + + SELECT date, track_name, driver_id, driver_username, driver_color, free_practice_point AS session_point + FROM all_free_practice_points + ) + WHERE driver_id = 5 +) +GROUP BY date, track_name, driver_id, driver_username, driver_color, cumulative_points + +UNION ALL + +SELECT * +FROM +( + SELECT date, track_name, driver_id, driver_username, driver_color, SUM(session_point) OVER (ORDER BY date, track_name) AS cumulative_points + FROM + ( + SELECT date, track_name, driver_id, driver_username, driver_color, race_point AS session_point + FROM all_race_points + + UNION ALL + + SELECT date, track_name, driver_id, driver_username, driver_color, sprint_point AS session_point + FROM all_sprint_points + + UNION ALL + + SELECT date, track_name, driver_id, driver_username, driver_color, qualifying_point AS session_point + FROM all_qualifying_points + + UNION ALL + + SELECT date, track_name, driver_id, driver_username, driver_color, free_practice_point AS session_point + FROM all_free_practice_points + ) + WHERE driver_id = 6 +) +GROUP BY date, track_name, driver_id, driver_username, driver_color, cumulative_points + + + + + + + + +/**************/ +/* NEXT TRACK */ +/**************/ +SELECT outer_table_tracks.track_id, outer_table_tracks.name, outer_table_tracks.date, outer_table_tracks.has_sprint, outer_table_tracks.country, outer_table_tracks.besttime_driver_time, +outer_table_drivers.username +FROM +( + SELECT * + FROM tracks + LEFT JOIN + ( + SELECT * + FROM gran_prix + ) AS inner_table + ON tracks.id = inner_table.track_id +) AS outer_table_tracks +LEFT JOIN +( + SELECT * + FROM drivers +) AS outer_table_drivers +ON outer_table_tracks.besttime_driver_id = outer_table_drivers.id +ORDER BY date ASC + + + + + +/*************/ +/* ALL FANTA */ +/*************/ +WITH all_session_points AS +( + SELECT inner_table_tracks.name AS track_name, gran_prix.date as gran_prix_date, inner_table_tracks.country AS track_country, + inner_table_race."race_id" AS race_id, inner_table_race."1_place_id" AS driver_race_1_place, inner_table_race."2_place_id" AS driver_race_2_place, inner_table_race."3_place_id" AS driver_race_3_place, inner_table_race."4_place_id" AS driver_race_4_place, inner_table_race."5_place_id" AS driver_race_5_place, inner_table_race."6_place_id" AS driver_race_6_place, inner_table_race."fast_lap_id" AS driver_race_fast_lap + FROM gran_prix + LEFT JOIN + ( + SELECT first_table.race_id, first_table.race_dnf, first_table."1_place_id", second_table."2_place_id", third_table."3_place_id", fourth_table."4_place_id", fifth_table."5_place_id", sixth_table."6_place_id", fast_lap_table."fast_lap_id" + FROM + ( + SELECT race_results.id AS race_id, race_results.dnf AS race_dnf, drivers.id AS "1_place_id" + FROM race_results + LEFT JOIN drivers + ON race_results."1_place_id" = drivers.id + ) AS first_table + INNER JOIN + ( + SELECT race_results.id AS race_id, drivers.id AS "2_place_id" + FROM race_results + LEFT JOIN drivers + ON race_results."2_place_id" = drivers.id + ) AS second_table + ON first_table.race_id = second_table.race_id + INNER JOIN + ( + SELECT race_results.id AS race_id, drivers.id AS "3_place_id" + FROM race_results + LEFT JOIN drivers + ON race_results."3_place_id" = drivers.id + ) AS third_table + ON first_table.race_id = third_table.race_id + INNER JOIN + ( + SELECT race_results.id AS race_id, drivers.id AS "4_place_id" + FROM race_results + LEFT JOIN drivers + ON race_results."4_place_id" = drivers.id + ) AS fourth_table + ON first_table.race_id = fourth_table.race_id + INNER JOIN + ( + SELECT race_results.id AS race_id, drivers.id AS "5_place_id" + FROM race_results + LEFT JOIN drivers + ON race_results."5_place_id" = drivers.id + ) AS fifth_table + ON first_table.race_id = fifth_table.race_id + INNER JOIN + ( + SELECT race_results.id AS race_id, drivers.id AS "6_place_id" + FROM race_results + LEFT JOIN drivers + ON race_results."6_place_id" = drivers.id + ) AS sixth_table + ON first_table.race_id = sixth_table.race_id + INNER JOIN + ( + SELECT race_results.id AS race_id, drivers.id AS "fast_lap_id" + FROM race_results + LEFT JOIN drivers + ON race_results."fast_lap_id" = drivers.id + ) AS fast_lap_table + ON first_table.race_id = fast_lap_table.race_id + ) AS inner_table_race + ON gran_prix.race_results_id = inner_table_race.race_id + LEFT JOIN + ( + SELECT * + FROM tracks + ) AS inner_table_tracks + ON gran_prix.track_id = inner_table_tracks.id +), +all_fanta_picks AS +( + SELECT * + FROM fanta + LEFT JOIN + ( + SELECT * + FROM fanta_player + ) AS inner_table + ON fanta.fanta_player_id = inner_table.id +) + +SELECT track_name, driver_race_1_place, driver_race_2_place, driver_race_3_place, driver_race_4_place, driver_race_5_place, driver_race_6_place, driver_race_fast_lap, +fanta_player_id, "1_place_pick", "2_place_pick", "3_place_pick", "4_place_pick", "5_place_pick", "6_place_pick", fast_lap_pick, username, name, surname +FROM all_session_points +LEFT JOIN +( + SELECT race_id, fanta_player_id, + "1_place_id" AS "1_place_pick", + "2_place_id" AS "2_place_pick", + "3_place_id" AS "3_place_pick", + "4_place_id" AS "4_place_pick", + "5_place_id" AS "5_place_pick", + "6_place_id" AS "6_place_pick", + fast_lap_id AS "fast_lap_pick", + username, name, surname + FROM all_fanta_picks +) AS inner_table +ON all_session_points.race_id = inner_table.race_id + +/* +fanta vote +*/ +SELECT + fp.id AS fantaPlayerId, + f.race_id as raceId, + fp.username as username, + f."1_place_id" as place1Id, + f."2_place_id" as place2Id, + f."3_place_id" as place3Id, + f."4_place_id" as place4Id, + f."5_place_id" as place5Id, + f."6_place_id" as place6Id +FROM + public.fanta_player fp +JOIN + public.fanta f ON fp.id = f.fanta_player_id +ORDER BY + fp.id, f.race_id; +/* + race resoult for fanta +*/ +select + rr.id as id, + rr."1_place_id" as place1Id, + rr."2_place_id" as place2Id, + rr."3_place_id" as place3Id, + rr."4_place_id" as place4Id, + rr."5_place_id" as place5Id, + rr."6_place_id" as place6Id +from + public.race_results rr +where + session_type_id = 1; + +/* + User for login and leaderboard +*/ +SELECT + id, + username, + "name", + surname, + "password" +FROM + public.fanta_player; \ No newline at end of file diff --git a/server/src/config/db.ts b/server/src/config/db.ts new file mode 100644 index 000000000..538448acb --- /dev/null +++ b/server/src/config/db.ts @@ -0,0 +1,33 @@ +import pg from 'pg'; +import dotenv from 'dotenv'; +import logger from './logger.js'; + +// Load environment variables +dotenv.config(); + +const { Pool } = pg; + +// Create a singleton database connection pool +const pool = new Pool({ + connectionString: process.env.RACEFORFEDERICA_DB_DATABASE_URL, + max: 20, // Maximum number of clients in the pool + idleTimeoutMillis: 30000, // Close idle clients after 30 seconds + connectionTimeoutMillis: 2000, // Return an error after 2 seconds if connection could not be established +}); + +// Handle pool errors +pool.on('error', (err) => { + logger.error('Unexpected error on idle database client', { error: err.message, stack: err.stack }); + process.exit(-1); +}); + +// Test the connection +pool.query('SELECT NOW()', (err, res) => { + if (err) { + logger.error('Database connection error', { error: err.message, stack: err.stack }); + } else { + logger.info('Database connected successfully', { timestamp: res.rows[0].now }); + } +}); + +export default pool; diff --git a/server/src/config/email_templates.ts b/server/src/config/email_templates.ts new file mode 100644 index 000000000..8c629fae3 --- /dev/null +++ b/server/src/config/email_templates.ts @@ -0,0 +1,240 @@ +/** + * Email Templates for Race for Federica + * + * This file contains all email templates used throughout the application. + * Templates are organized by function and include both HTML and plain text versions. + */ + +interface Race { + track_name: string; + country: string; + date: string; + has_sprint: number; + has_x2: number; +} + +interface User { + username: string; + name?: string; + surname?: string; +} + +/** + * Generate email template for password reset by admin + */ +export function getPasswordResetEmailTemplate(user: User, newPassword: string): { html: string; text: string } { + const htmlMessage = ` + + + + + + Race for Federica - Password Modificata + + +
+

+ Race for Federica Logo + Race for Federica + Race for Federica Logo +

+

Fantasy F1 Championship

+
+ +
+

Ciao ${user.username}! 👋

+ +

+ Un amministratore ha modificato la tua password su Race for Federica. +

+ +
+
🔐 Nuova Password Temporanea
+
+ ${newPassword} +
+
+ +
+
⚠️ Importante - Sicurezza
+

+ Cambia la Password al primo accesso +

+
+ + + +

+ Dopo aver effettuato l'accesso, puoi cambiare la tua password dalle impostazioni del profilo. +

+ +
+
+

+ Team Race for Federica +

+
+
+
+ +
+

+ Questa è una email automatica. Non rispondere a questo messaggio. +

+

+ Se non hai richiesto questa modifica, contatta immediatamente un amministratore. +

+
+ + + `; + + const textMessage = ` + Race for Federica - Fantasy F1 Championship + + Ciao ${user.username}! + + Un amministratore ha modificato la tua password su Race for Federica. + + 🔐 Nuova Password Temporanea: ${newPassword} + + ⚠️ IMPORTANTE - SICUREZZA: + Cambia la Password al primo accesso + + Accedi ora su: https://f123dashboard.app.genez.io + + Dopo aver effettuato l'accesso, puoi cambiare la tua password dalle impostazioni del profilo. + + --- + Team Race for Federica + Questa è una email automatica. Non rispondere a questo messaggio. + Se non hai richiesto questa modifica, contatta immediatamente un amministratore. + `; + + return { + html: htmlMessage, + text: textMessage.trim() + }; +} + +/** + * Generate email template for upcoming races notification + */ +export function getUpcomingRaceEmailTemplate(user: User, races: Race[]): { html: string; text: string; subject: string } { + // Prepare race details for HTML + const raceDetailsHtml = races.map(race => { + const raceDate = new Date(race.date); + const sprintInfo = race.has_sprint === 1 ? " (con Sprint)" : ""; + const multiplierInfo = race.has_x2 === 1 ? " - PUNTI DOPPI!" : ""; + return ` +
+
🏁 ${race.track_name}, ${race.country}${sprintInfo}${multiplierInfo}
+
⏰ Inizio oggi alle: ${raceDate.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' })}
+
+ `; + }).join(''); + + // Prepare race details for plain text + const raceDetailsText = races.map(race => { + const raceDate = new Date(race.date); + const sprintInfo = race.has_sprint === 1 ? " (con Sprint)" : ""; + const multiplierInfo = race.has_x2 === 1 ? " - PUNTI DOPPI!" : ""; + return `${race.track_name}, ${race.country}${sprintInfo}${multiplierInfo}\nInizio oggi alle: ${raceDate.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' })}`; + }).join('\n\n'); + + const subject = `Gara in arrivo - ${races.length} gara/e oggi su Race for Federica`; + + const htmlMessage = ` + + + + + + Race for Federica - Avviso Gara + + +
+

+ Race for Federica Logo + Race for Federica + Race for Federica Logo +

+

Fantasy F1 Championship

+
+ +
+

Ciao ${user.username}! 👋

+ +

+ Le seguenti gare stanno per iniziare: +

+ + ${raceDetailsHtml} + + + +

+ Buona fortuna e che vinca il pilota più veloce! 🏆 +

+ +
+
+

+ Team Race for Federica +

+
+
+
+ +
+

+ Questa è una email automatica. Non rispondere a questo messaggio. +

+
+ + + `; + + const textMessage = ` + Race for Federica - Fantasy F1 Championship + + Ciao ${user.username}! + + Le seguenti gare stanno per iniziare: + + ${raceDetailsText} + + Vota la tua squadra su: https://f123dashboard.app.genez.io/#/fanta + + Buona fortuna e che vinca il pilota più veloce! + + --- + Team Race for Federica + Questa è una email automatica. Non rispondere a questo messaggio. + Race for Federica + `; + + return { + html: htmlMessage, + text: textMessage.trim(), + subject: subject + }; +} diff --git a/server/src/config/logger.ts b/server/src/config/logger.ts new file mode 100644 index 000000000..bc3eb03eb --- /dev/null +++ b/server/src/config/logger.ts @@ -0,0 +1,103 @@ +import winston from 'winston'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +import { existsSync, mkdirSync } from 'fs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Define log levels +const levels = { + error: 0, + warn: 1, + info: 2, + http: 3, + debug: 4, +}; + +// Define log colors +const colors = { + error: 'red', + warn: 'yellow', + info: 'green', + http: 'magenta', + debug: 'white', +}; + +// Tell winston about our colors +winston.addColors(colors); + +// Define log format for files (readable, no colors) +const fileFormat = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.errors({ stack: true }), + winston.format.splat(), + winston.format.printf( + (info) => { + const { timestamp, level, message, stack, ...meta } = info; + const metaStr = Object.keys(meta).length ? '\n' + JSON.stringify(meta, null, 2) : ''; + return `${timestamp} [${level.toUpperCase()}]: ${message}${stack ? '\n' + stack : ''}${metaStr}`; + } + ) +); + +// Console format (for development) +const consoleFormat = winston.format.combine( + winston.format.colorize({ all: true }), + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.printf( + (info) => `${info.timestamp} [${info.level}]: ${info.message}${info.stack ? '\n' + info.stack : ''}` + ) +); + +// Define which transports the logger should use +const transports: winston.transport[] = [ + // Console transport + new winston.transports.Console({ + format: consoleFormat, + }), +]; + +// Add file transports in production +if (process.env.NODE_ENV === 'production') { + const logDir = path.join(__dirname, '../../../logs'); + + // Error log + transports.push( + new winston.transports.File({ + filename: path.join(logDir, 'error.log'), + level: 'error', + format: fileFormat, + maxsize: 5242880, // 5MB + maxFiles: 5, + }) + ); + + // Combined log + transports.push( + new winston.transports.File({ + filename: path.join(logDir, 'combined.log'), + format: fileFormat, + maxsize: 5242880, // 5MB + maxFiles: 5, + }) + ); +} + +// Create the logger +const logger = winston.createLogger({ + level: process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'http' : 'debug'), + levels, + transports, + exitOnError: false, +}); + +// Create a stream object for Morgan (HTTP request logging) +export const stream = { + write: (message: string) => { + logger.http(message.trim()); + }, +}; + +export default logger; diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts new file mode 100644 index 000000000..c0a6b8cb4 --- /dev/null +++ b/server/src/controllers/auth.controller.ts @@ -0,0 +1,211 @@ +import { Request, Response } from 'express'; +import { AuthService } from '../services/auth.service.js'; +import pool from '../config/db.js'; +import logger from '../config/logger.js'; + +const authService = new AuthService(pool); + +export class AuthController { + async getUsers(req: Request, res: Response): Promise { + try { + const users = await authService.getUsers(); + res.json(users); + } catch (error) { + logger.error('Error getting users:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to get users' + }); + } + } + + async cleanupExpiredSessions(req: Request, res: Response): Promise { + try { + const result = await authService.cleanupExpiredSessions(); + res.json(result); + } catch (error) { + logger.error('Error cleaning up expired sessions:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to cleanup sessions' + }); + } + } + + async login(req: Request, res: Response): Promise { + try { + const loginData = { + ...req.body, + userAgent: req.headers['user-agent'] + }; + const result = await authService.login(loginData); + + if (result.success) { + res.json(result); + } else { + res.status(401).json(result); + } + } catch (error) { + logger.error('Error during login:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to login' + }); + } + } + + async register(req: Request, res: Response): Promise { + try { + const result = await authService.register(req.body); + + if (result.success) { + res.status(201).json(result); + } else { + res.status(400).json(result); + } + } catch (error) { + logger.error('Error during registration:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to register' + }); + } + } + + async validateToken(req: Request, res: Response): Promise { + try { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + res.status(401).json({ + valid: false, + message: 'No token provided' + }); + return; + } + + const token = authHeader.substring(7); // Remove 'Bearer ' prefix + const result = await authService.validateToken(token); + res.json(result); + } catch (error) { + logger.error('Error validating token:', error); + res.status(500).json({ + valid: false, + message: error instanceof Error ? error.message : 'Failed to validate token' + }); + } + } + + async changePassword(req: Request, res: Response): Promise { + try { + const result = await authService.changePassword(req.body); + + if (result.success) { + res.json(result); + } else { + res.status(400).json(result); + } + } catch (error) { + logger.error('Error changing password:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to change password' + }); + } + } + + async adminChangePassword(req: Request, res: Response): Promise { + try { + const result = await authService.adminChangePassword(req.body); + res.json(result); + } catch (error) { + logger.error('Error in admin change password:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to change password' + }); + } + } + + async refreshToken(req: Request, res: Response): Promise { + try { + const userAgent = req.headers['user-agent']; + const result = await authService.refreshToken(req.user!.jwtToken, userAgent); + + if (result.success) { + res.json(result); + } else { + res.status(401).json(result); + } + } catch (error) { + logger.error('Error refreshing token:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to refresh token' + }); + } + } + + async logout(req: Request, res: Response): Promise { + try { + const result = await authService.logout(req.user!.jwtToken); + res.json(result); + } catch (error) { + logger.error('Error during logout:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to logout' + }); + } + } + + async logoutAllSessions(req: Request, res: Response): Promise { + try { + const result = await authService.logoutAllSessions(req.user!.jwtToken); + res.json(result); + } catch (error) { + logger.error('Error logging out all sessions:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to logout all sessions' + }); + } + } + + async getUserSessions(req: Request, res: Response): Promise { + try { + const result = await authService.getUserSessions(req.user!.jwtToken); + + if (result.success) { + res.json(result); + } else { + res.status(400).json(result); + } + } catch (error) { + logger.error('Error getting user sessions:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to get sessions' + }); + } + } + + async updateUserInfo(req: Request, res: Response): Promise { + try { + const result = await authService.updateUserInfo(req.body, req.user!.jwtToken); + + if (result.success) { + res.json(result); + } else { + res.status(400).json(result); + } + } catch (error) { + logger.error('Error updating user info:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to update user info' + }); + } + } +} + +export const authController = new AuthController(); diff --git a/server/src/controllers/database.controller.ts b/server/src/controllers/database.controller.ts new file mode 100644 index 000000000..13ed0f544 --- /dev/null +++ b/server/src/controllers/database.controller.ts @@ -0,0 +1,175 @@ +import { Request, Response } from 'express'; +import { DatabaseService } from '../services/database.service.js'; +import pool from '../config/db.js'; +import logger from '../config/logger.js'; + +const databaseService = new DatabaseService(pool); + +export class DatabaseController { + async getAllDrivers(req: Request, res: Response): Promise { + try { + const seasonId = req.body?.seasonId ? parseInt(req.body.seasonId) : undefined; + const drivers = await databaseService.getAllDrivers(seasonId); + res.json(drivers); + } catch (error) { + logger.error('Error getting all drivers:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to get drivers' + }); + } + } + + async getDriversData(req: Request, res: Response): Promise { + try { + const seasonId = req.body?.seasonId ? parseInt(req.body.seasonId) : undefined; + const drivers = await databaseService.getDriversData(seasonId); + res.json(drivers); + } catch (error) { + logger.error('Error getting drivers data:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to get drivers data' + }); + } + } + + async getChampionship(req: Request, res: Response): Promise { + try { + const seasonId = req.body?.seasonId ? parseInt(req.body.seasonId) : undefined; + const championship = await databaseService.getChampionship(seasonId); + res.json(championship); + } catch (error) { + logger.error('Error getting championship:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to get championship' + }); + } + } + + async getCumulativePoints(req: Request, res: Response): Promise { + try { + const seasonId = req.body?.seasonId ? parseInt(req.body.seasonId) : undefined; + const points = await databaseService.getCumulativePoints(seasonId); + res.json(points); + } catch (error) { + logger.error('Error getting cumulative points:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to get cumulative points' + }); + } + } + + async getAllTracks(req: Request, res: Response): Promise { + try { + const seasonId = req.body?.seasonId ? parseInt(req.body.seasonId) : undefined; + const tracks = await databaseService.getAllTracks(seasonId); + res.json(tracks); + } catch (error) { + logger.error('Error getting all tracks:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to get tracks' + }); + } + } + + async getRaceResult(req: Request, res: Response): Promise { + try { + const seasonId = req.body?.seasonId ? parseInt(req.body.seasonId) : undefined; + const results = await databaseService.getRaceResult(seasonId); + res.json(results); + } catch (error) { + logger.error('Error getting race results:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to get race results' + }); + } + } + + async getAllSeasons(req: Request, res: Response): Promise { + try { + const seasons = await databaseService.getAllSeasons(); + res.json(seasons); + } catch (error) { + logger.error('Error getting all seasons:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to get seasons' + }); + } + } + + async getConstructors(req: Request, res: Response): Promise { + try { + const seasonId = req.body?.seasonId ? parseInt(req.body.seasonId) : undefined; + const constructors = await databaseService.getConstructors(seasonId); + res.json(constructors); + } catch (error) { + logger.error('Error getting constructors:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to get constructors' + }); + } + } + + async getConstructorGrandPrixPoints(req: Request, res: Response): Promise { + try { + const seasonId = req.body?.seasonId ? parseInt(req.body.seasonId) : undefined; + const points = await databaseService.getConstructorGrandPrixPoints(seasonId); + res.json(points); + } catch (error) { + logger.error('Error getting constructor grand prix points:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to get constructor points' + }); + } + } + + async setGpResult(req: Request, res: Response): Promise { + try { + const { + trackId, + hasSprint, + raceResult, + raceDnfResult, + sprintResult, + sprintDnfResult, + qualiResult, + fpResult, + seasonId + } = req.body; + + const result = await databaseService.setGpResult( + trackId, + hasSprint, + raceResult, + raceDnfResult, + sprintResult, + sprintDnfResult, + qualiResult, + fpResult, + seasonId + ); + + if (result.success) { + res.json(result); + } else { + res.status(400).json(result); + } + } catch (error) { + logger.error('Error setting GP result:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to set GP result' + }); + } + } +} + +export const databaseController = new DatabaseController(); diff --git a/server/src/controllers/deploy.controller.ts b/server/src/controllers/deploy.controller.ts new file mode 100644 index 000000000..c31062951 --- /dev/null +++ b/server/src/controllers/deploy.controller.ts @@ -0,0 +1,44 @@ +import { Request, Response } from 'express'; +import { exec } from 'child_process'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import logger from '../config/logger.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export class DeployController { + async deploy(req: Request, res: Response): Promise { + const deploySecret = req.headers['x-deploy-secret']; + // 1. Validate Secret + if (deploySecret !== process.env.DEPLOY_SECRET) { + logger.warn('⛔ Unauthorized deployment attempt', { ip: req.ip }); + logger.warn(`Provided secret: ${deploySecret} Expected secret: ${process.env.DEPLOY_SECRET}`); + res.status(401).json({ success: false, message: 'Unauthorized' }); + return; + } + + logger.info('🚀 Deployment webhook received. Triggering deployment script...'); + + // 2. Respond immediately to avoid timeout + res.json({ success: true, message: 'Deployment started' }); + + // 3. Execute Script + // Go up from src/controllers to root/scripts + const scriptPath = path.resolve(__dirname, '../../../deploy.sh'); + + exec(`bash "${scriptPath}"`, (error, stdout, stderr) => { + if (error) { + logger.error(`❌ Deployment script log: ${stdout}`); + logger.error(`❌ Deployment failed: ${error.message}`); + return; + } + if (stderr) { + logger.warn(`Deployment stderr: ${stderr}`); + } + logger.info(`✅ Deployment success: ${stdout}`); + }); + } +} + +export const deployController = new DeployController(); diff --git a/server/src/controllers/fanta.controller.ts b/server/src/controllers/fanta.controller.ts new file mode 100644 index 000000000..4e64a0843 --- /dev/null +++ b/server/src/controllers/fanta.controller.ts @@ -0,0 +1,37 @@ +import { Request, Response } from 'express'; +import { FantaService } from '../services/fanta.service.js'; +import pool from '../config/db.js'; +import logger from '../config/logger.js'; + +const fantaService = new FantaService(pool); + +export class FantaController { + async getFantaVote(req: Request, res: Response): Promise { + try { + const seasonId = req.body?.seasonId ? parseInt(req.body.seasonId) : undefined; + const votes = await fantaService.getFantaVote(seasonId); + res.json(votes); + } catch (error) { + logger.error('Error getting fanta votes:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to get fanta votes' + }); + } + } + + async setFantaVoto(req: Request, res: Response): Promise { + try { + const result = await fantaService.setFantaVoto(req.body); + res.json(result); + } catch (error) { + logger.error('Error setting fanta vote:', error); + res.status(400).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to set fanta vote' + }); + } + } +} + +export const fantaController = new FantaController(); diff --git a/server/src/controllers/gp-edit.controller.ts b/server/src/controllers/gp-edit.controller.ts new file mode 100644 index 000000000..ea34311f6 --- /dev/null +++ b/server/src/controllers/gp-edit.controller.ts @@ -0,0 +1,106 @@ +import { Request, Response } from 'express'; +import { GpEditService } from '../services/gp-edit.service.js'; +import pool from '../config/db.js'; +import logger from '../config/logger.js'; + +const gpEditService = new GpEditService(pool); + +export class GpEditController { + async getUpcomingGps(req: Request, res: Response): Promise { + try { + const data = await gpEditService.getUpcomingGps(); + res.json({ success: true, data }); + } catch (error) { + logger.error('Error getting upcoming GPs:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to get upcoming GPs' + }); + } + } + + async getAllTracks(req: Request, res: Response): Promise { + try { + const data = await gpEditService.getAllTracks(); + res.json({ success: true, data }); + } catch (error) { + logger.error('Error getting all tracks:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to get all tracks' + }); + } + } + + async createGp(req: Request, res: Response): Promise { + try { + const data = await gpEditService.createGp(req.body); + res.json({ success: true, data }); + } catch (error) { + logger.error('Error creating GP:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to create GP' + }); + } + } + + async updateGp(req: Request, res: Response): Promise { + try { + const id = parseInt(req.params.id as string); + if (isNaN(id)) { + res.status(400).json({ success: false, message: 'Invalid ID' }); + return; + } + + const data = await gpEditService.updateGp(id, req.body); + res.json({ success: true, data }); + } catch (error) { + logger.error('Error updating GP:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to update GP' + }); + } + } + + async deleteGp(req: Request, res: Response): Promise { + try { + const id = parseInt(req.params.id as string); + if (isNaN(id)) { + res.status(400).json({ success: false, message: 'Invalid ID' }); + return; + } + + await gpEditService.deleteGp(id); + res.json({ success: true }); + } catch (error) { + logger.error('Error deleting GP:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to delete GP' + }); + } + } + + async bulkUpdateGpDate(req: Request, res: Response): Promise { + try { + const daysOffset = parseInt(req.body.daysOffset); + if (isNaN(daysOffset)) { + res.status(400).json({ success: false, message: 'Invalid daysOffset' }); + return; + } + + await gpEditService.bulkUpdateGpDate(daysOffset); + res.json({ success: true }); + } catch (error) { + logger.error('Error bulk updating GP dates:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to bulk update GP dates' + }); + } + } +} + +export const gpEditController = new GpEditController(); diff --git a/server/src/controllers/playground.controller.ts b/server/src/controllers/playground.controller.ts new file mode 100644 index 000000000..79730881a --- /dev/null +++ b/server/src/controllers/playground.controller.ts @@ -0,0 +1,39 @@ +import { Request, Response } from 'express'; +import { PlaygroundService } from '../services/playground.service.js'; +import pool from '../config/db.js'; +import logger from '../config/logger.js'; + +const playgroundService = new PlaygroundService(pool); + +export class PlaygroundController { + async getPlaygroundLeaderboard(req: Request, res: Response): Promise { + try { + const leaderboard = await playgroundService.getPlaygroundLeaderboard(); + res.json(leaderboard); + } catch (error) { + logger.error('Error getting playground leaderboard:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to get leaderboard' + }); + } + } + + async setUserBestScore(req: Request, res: Response): Promise { + try { + await playgroundService.setUserBestScore(req.body); + res.json({ + success: true, + message: 'Best score saved successfully' + }); + } catch (error) { + logger.error('Error setting best score:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to set best score' + }); + } + } +} + +export const playgroundController = new PlaygroundController(); diff --git a/server/src/controllers/twitch.controller.ts b/server/src/controllers/twitch.controller.ts new file mode 100644 index 000000000..40b2386d4 --- /dev/null +++ b/server/src/controllers/twitch.controller.ts @@ -0,0 +1,32 @@ +import { Request, Response } from 'express'; +import { TwitchService } from '../services/twitch.service.js'; +import logger from '../config/logger.js'; + +const twitchService = new TwitchService(); + +export class TwitchController { + async getStreamInfo(req: Request, res: Response): Promise { + try { + const { channelName } = req.body; + + if (!channelName) { + res.status(400).json({ + success: false, + message: 'Channel name is required' + }); + return; + } + + const streamInfo = await twitchService.getStreamInfo(channelName); + res.json(streamInfo); + } catch (error) { + logger.error('Error getting stream info:', error); + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : 'Failed to get stream info' + }); + } + } +} + +export const twitchController = new TwitchController(); diff --git a/server/src/middleware/auth.middleware.ts b/server/src/middleware/auth.middleware.ts new file mode 100644 index 000000000..8c7c65152 --- /dev/null +++ b/server/src/middleware/auth.middleware.ts @@ -0,0 +1,108 @@ +import { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; + +// Extend Express Request type to include user data +declare global { + namespace Express { + interface Request { + user?: { + userId: number; + username: string; + isAdmin?: boolean; + jwtToken: string; + }; + } + } +} + +export const authMiddleware = (req: Request, res: Response, next: NextFunction): void => { +try { + // Get token from Authorization header + const authHeader = req.headers.authorization; + + if (!authHeader) { + res.status(401).json({ + success: false, + message: 'Token di autenticazione mancante' + }); + return; + } + + // Extract token (format: "Bearer TOKEN") + const token = authHeader.startsWith('Bearer ') + ? authHeader.substring(7) + : authHeader; + + if (!token) { + res.status(401).json({ + success: false, + message: 'Token di autenticazione non valido' + }); + return; + } + + // Verify JWT token + const jwtSecret = process.env.JWT_SECRET; + if (!jwtSecret) { + throw new Error('JWT_SECRET not configured'); + } + + const decoded = jwt.verify(token, jwtSecret) as { + userId: number; + username: string; + isAdmin?: boolean; + }; + + // Attach user data to request + req.user = { + userId: decoded.userId, + username: decoded.username, + isAdmin: decoded.isAdmin, + jwtToken: token + }; + next(); + } catch (error) { + if (error instanceof jwt.TokenExpiredError) { + res.status(401).json({ + success: false, + message: 'Token scaduto' + }); + return; + } + + if (error instanceof jwt.JsonWebTokenError) { + res.status(401).json({ + success: false, + message: 'Token non valido' + }); + return; + } + + console.error('Auth middleware error:', error); + res.status(500).json({ + success: false, + message: 'Errore durante la verifica del token' + }); + } +}; + +// Admin-only middleware +export const adminMiddleware = (req: Request, res: Response, next: NextFunction): void => { + if (!req.user) { + res.status(401).json({ + success: false, + message: 'Autenticazione richiesta' + }); + return; + } + + if (!req.user.isAdmin) { + res.status(403).json({ + success: false, + message: 'Accesso negato: permessi amministratore richiesti' + }); + return; + } + + next(); +}; diff --git a/server/src/routes/auth.routes.ts b/server/src/routes/auth.routes.ts new file mode 100644 index 000000000..be03c0053 --- /dev/null +++ b/server/src/routes/auth.routes.ts @@ -0,0 +1,26 @@ +import { Router } from 'express'; +import { authController } from '../controllers/auth.controller.js'; +import { authMiddleware, adminMiddleware } from '../middleware/auth.middleware.js'; + +const router = Router(); + +// Public endpoints (no auth required) +router.post('/login', (req, res) => authController.login(req, res)); +router.post('/register', (req, res) => authController.register(req, res)); +router.post('/validate', (req, res) => authController.validateToken(req, res)); +router.post('/users', (req, res) => authController.getUsers(req, res)); + +// Protected endpoints (requires authentication) +router.post('/refresh-token', authMiddleware, (req, res) => authController.refreshToken(req, res)); +router.post('/logout', authMiddleware, (req, res) => authController.logout(req, res)); +router.post('/logout-all', authMiddleware, (req, res) => authController.logoutAllSessions(req, res)); +router.post('/sessions', authMiddleware, (req, res) => authController.getUserSessions(req, res)); +router.post('/change-password', authMiddleware, (req, res) => authController.changePassword(req, res)); +router.post('/update-user-info', authMiddleware, (req, res) => authController.updateUserInfo(req, res)); + +// Admin-only endpoints (requires authentication + admin privileges) + +router.post('/admin-change-password', authMiddleware, adminMiddleware, (req, res) => authController.adminChangePassword(req, res)); +router.post('/cleanup-sessions', authMiddleware, adminMiddleware, (req, res) => authController.cleanupExpiredSessions(req, res)); + +export { router as authRouter }; diff --git a/server/src/routes/database.routes.ts b/server/src/routes/database.routes.ts new file mode 100644 index 000000000..761e9c715 --- /dev/null +++ b/server/src/routes/database.routes.ts @@ -0,0 +1,32 @@ +import { Router } from 'express'; +import { databaseController } from '../controllers/database.controller.js'; +import { authMiddleware, adminMiddleware } from '../middleware/auth.middleware.js'; + +const router = Router(); + +// Public read endpoints (no authentication required) +// Driver endpoints +router.post('/drivers', (req, res) => databaseController.getAllDrivers(req, res)); +router.post('/drivers-data', (req, res) => databaseController.getDriversData(req, res)); + +// Championship endpoints +router.post('/championship', (req, res) => databaseController.getChampionship(req, res)); +router.post('/cumulative-points', (req, res) => databaseController.getCumulativePoints(req, res)); + +// Track endpoints +router.post('/tracks', (req, res) => databaseController.getAllTracks(req, res)); + +// Race result endpoints +router.post('/race-results', (req, res) => databaseController.getRaceResult(req, res)); + +// Season endpoints +router.post('/seasons', (req, res) => databaseController.getAllSeasons(req, res)); + +// Constructor endpoints +router.post('/constructors', (req, res) => databaseController.getConstructors(req, res)); +router.post('/constructor-grand-prix-points', (req, res) => databaseController.getConstructorGrandPrixPoints(req, res)); + +// Admin-only write endpoints (requires authentication + admin privileges) +router.post('/set-gp-result', authMiddleware, adminMiddleware, (req, res) => databaseController.setGpResult(req, res)); + +export { router as databaseRouter }; diff --git a/server/src/routes/deploy.routes.ts b/server/src/routes/deploy.routes.ts new file mode 100644 index 000000000..9a8c82903 --- /dev/null +++ b/server/src/routes/deploy.routes.ts @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import { deployController } from '../controllers/deploy.controller.js'; + +const router = Router(); + +router.post('/', (req, res) => deployController.deploy(req, res)); + +export { router as deployRouter }; diff --git a/server/src/routes/fanta.routes.ts b/server/src/routes/fanta.routes.ts new file mode 100644 index 000000000..3c55dbbdf --- /dev/null +++ b/server/src/routes/fanta.routes.ts @@ -0,0 +1,14 @@ +import { Router } from 'express'; +import { fantaController } from '../controllers/fanta.controller.js'; +import { authMiddleware } from '../middleware/auth.middleware.js'; + +const router = Router(); + +// Fanta endpoints no authentication +router.post('/votes', (req, res) => fantaController.getFantaVote(req, res)); + +// fanta endpoints require authentication (user-specific data) +router.post('/votes', (req, res) => fantaController.getFantaVote(req, res)); +router.post('/set-vote', authMiddleware, (req, res) => fantaController.setFantaVoto(req, res)); + +export { router as fantaRouter }; diff --git a/server/src/routes/gp-edit.routes.ts b/server/src/routes/gp-edit.routes.ts new file mode 100644 index 000000000..36ae0a85a --- /dev/null +++ b/server/src/routes/gp-edit.routes.ts @@ -0,0 +1,15 @@ +import { Router } from 'express'; +import { authMiddleware, adminMiddleware } from '../middleware/auth.middleware.js'; +import { gpEditController } from '../controllers/gp-edit.controller.js'; + +export const gpEditRouter = Router(); + +// Retrieve current data +gpEditRouter.post('/list', authMiddleware, adminMiddleware, (req, res) => gpEditController.getUpcomingGps(req, res)); +gpEditRouter.post('/tracks', authMiddleware, adminMiddleware, (req, res) => gpEditController.getAllTracks(req, res)); + +// Actions +gpEditRouter.post('/create', authMiddleware, adminMiddleware, (req, res) => gpEditController.createGp(req, res)); +gpEditRouter.post('/update/:id', authMiddleware, adminMiddleware, (req, res) => gpEditController.updateGp(req, res)); +gpEditRouter.post('/delete/:id', authMiddleware, adminMiddleware, (req, res) => gpEditController.deleteGp(req, res)); +gpEditRouter.post('/bulk-update-date', authMiddleware, adminMiddleware, (req, res) => gpEditController.bulkUpdateGpDate(req, res)); diff --git a/server/src/routes/playground.routes.ts b/server/src/routes/playground.routes.ts new file mode 100644 index 000000000..4ee102ed5 --- /dev/null +++ b/server/src/routes/playground.routes.ts @@ -0,0 +1,13 @@ +import { Router } from 'express'; +import { playgroundController } from '../controllers/playground.controller.js'; +import { authMiddleware } from '../middleware/auth.middleware.js'; + +const router = Router(); + +// Public endpoint - anyone can view leaderboard +router.post('/leaderboard', (req, res) => playgroundController.getPlaygroundLeaderboard(req, res)); + +// Protected endpoint - only authenticated users can submit scores +router.post('/score', authMiddleware, (req, res) => playgroundController.setUserBestScore(req, res)); + +export { router as playgroundRouter }; diff --git a/server/src/routes/twitch.routes.ts b/server/src/routes/twitch.routes.ts new file mode 100644 index 000000000..b109d550c --- /dev/null +++ b/server/src/routes/twitch.routes.ts @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import { twitchController } from '../controllers/twitch.controller.js'; + +const router = Router(); + +router.post('/stream-info', (req, res) => twitchController.getStreamInfo(req, res)); + +export { router as twitchRouter }; diff --git a/server/src/server.ts b/server/src/server.ts new file mode 100644 index 000000000..8d5ffa699 --- /dev/null +++ b/server/src/server.ts @@ -0,0 +1,117 @@ +import express, { Request, Response, NextFunction } from 'express'; +import cors from 'cors'; +import path from 'path'; +import dotenv from 'dotenv'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +import morgan from 'morgan'; +import cron from 'node-cron'; +import logger, { stream } from './config/logger.js'; +import { EmailService } from './services/mail.service.js'; + +// Load environment variables +process.env.NODE_ENV === 'production' ? dotenv.config({override: true }): dotenv.config({override: true }); +// Get current directory in ES module +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Middleware +app.use(express.json({ limit: '50mb' })); +app.use(express.urlencoded({ extended: true, limit: '50mb' })); +app.use(cors()); + +// HTTP request logging with Morgan + Winston +app.use(morgan( + ':method :url :status :res[content-length] - :response-time ms', + { stream } +)); + +// Health check endpoint +app.get('/api/health', (req: Request, res: Response) => { + res.json({ + status: 'ok', + timestamp: new Date().toISOString(), + environment: process.env.NODE_ENV || 'development' + }); +}); + +// API Routes +import { databaseRouter } from './routes/database.routes.js'; +import { authRouter } from './routes/auth.routes.js'; +import { fantaRouter } from './routes/fanta.routes.js'; +import { twitchRouter } from './routes/twitch.routes.js'; +import { playgroundRouter } from './routes/playground.routes.js'; +import { deployRouter } from './routes/deploy.routes.js'; +import { gpEditRouter } from './routes/gp-edit.routes.js'; +import { existsSync } from 'fs'; + +app.use('/api/database', databaseRouter); +app.use('/api/auth', authRouter); +app.use('/api/fanta', fantaRouter); +app.use('/api/twitch', twitchRouter); +app.use('/api/playground', playgroundRouter); +app.use('/api/deploy', deployRouter); +app.use('/api/gp-edit', gpEditRouter); + + +// Serve Angular static files in production +if (process.env.NODE_ENV === 'production') { + // Production build structure: dist/server/server.js -> dist/client/browser/ + const angularDistPath = path.join(__dirname, '../client/browser'); + + logger.info(`📂 Attempting to serve static files from: ${angularDistPath}`); + + if (existsSync(angularDistPath)) { + app.use(express.static(angularDistPath)); + + // All other routes return the Angular app + app.get('*', (req: Request, res: Response) => { + res.sendFile(path.join(angularDistPath, 'index.html')); + }); + + logger.info(`✅ Serving Angular static files from: ${angularDistPath}`); + } else { + logger.warn(`⚠️ Angular build not found at: ${angularDistPath}`); + } +} + +// Error handling middleware +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { + logger.error(`Error: ${err.message}`, { stack: err.stack, url: req.url, method: req.method }); + res.status(500).json({ + success: false, + message: process.env.NODE_ENV === 'production' + ? 'Internal server error' + : err.message, + ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }) + }); +}); + +// Initialize Email Service +const emailService = new EmailService(); + +// Schedule cron job to send upcoming race emails daily at 18:00 (6:00 PM) +cron.schedule('0 18 * * *', async () => { + logger.info('🕐 Running scheduled task: sendIncomingRaceMail'); + try { + await emailService.sendIncomingRaceMail(); + logger.info('✅ Scheduled email task completed successfully'); + } catch (error) { + logger.error('❌ Error running scheduled email task:', error); + } +}, { + timezone: 'Europe/Rome' // Adjust timezone as needed +}); + +// Start server +app.listen(PORT, () => { + logger.info(`🚀 Server running on http://localhost:${PORT}`); + logger.info(`📦 Environment: ${process.env.NODE_ENV || 'development'}`); + logger.info(`🔧 Health check: http://localhost:${PORT}/api/health`); + logger.info('⏰ Cron job scheduled: sendIncomingRaceMail at 18:00 daily'); +}); + +export default app; diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts new file mode 100644 index 000000000..1f30f5b68 --- /dev/null +++ b/server/src/services/auth.service.ts @@ -0,0 +1,1013 @@ +import pg from "pg"; +import { createHash, randomBytes } from "crypto"; +import jwt from "jsonwebtoken"; +import { EmailService } from "./mail.service.js"; +import { getPasswordResetEmailTemplate } from "../config/email_templates.js"; +import type { + User, + LoginRequest, + AuthResponse, + ChangePasswordRequest, + AdminChangePasswordRequest, + ChangePasswordResponse, + SessionsResponse, + TokenValidationResponse, + RefreshTokenResponse, + LogoutResponse, + RegisterRequest, + UpdateUserInfoRequest +} from "@f123dashboard/shared"; + +export class AuthService { + private pool: pg.Pool; + private jwtSecret: string; + private static readonly TOKEN_EXPIRY_DAYS = 7; + private static readonly TOKEN_EXPIRY_JWT = '7d'; + + constructor(pool: pg.Pool) { + this.pool = pool; + if (!process.env.JWT_SECRET) { + throw new Error('JWT_SECRET environment variable is required'); + } + this.jwtSecret = process.env.JWT_SECRET; + } + + async getUsers(): Promise { + const result = await this.pool.query (` + SELECT id, username, name, surname, mail, encode(image, 'escape') as image + FROM users; + `); + return result.rows as User[]; + } + + async cleanupExpiredSessions(): Promise { + try { + await this.cleanExpiredSessions(); + + return { + success: true, + message: 'Sessioni scadute pulite con successo' + }; + + } catch (error) { + console.error('Cleanup expired sessions error:', error); + return { + success: false, + message: 'Si è verificato un errore durante la pulizia delle sessioni scadute' + }; + } + } + + async login(loginData: LoginRequest): Promise { + try { + this.validateLoginInput(loginData.username, loginData.password); + + // Get user from database + const result = await this.pool.query( + 'SELECT id, username, name, surname, password, mail, encode(image, \'escape\') as image, is_active, is_admin FROM users WHERE username = $1', + [loginData.username] + ); + + if (result.rows.length === 0) { + return { + success: false, + message: 'Nome utente o password non validi' + }; + } + + const user = result.rows[0]; + + // Check if user account is active + if (!user.is_active) { + return { + success: false, + message: 'Account disabilitato' + }; + } + + // Verify password + const isPasswordValid = this.comparePassword(loginData.password, user.password); + + if (!isPasswordValid) { + return { + success: false, + message: 'Nome utente o password non validi' + }; + } + + // Create session and get JWT token + const jwtToken = await this.createSession(user.id, loginData.userAgent); + + // Update last login + await this.pool.query( + 'UPDATE users SET last_login = NOW() WHERE id = $1', + [user.id] + ); + + return { + success: true, + message: 'Login effettuato con successo', + user: { + id: user.id, + username: user.username, + name: user.name, + surname: user.surname, + mail: user.mail, + image: user.image, + isAdmin: user.is_admin + }, + token: jwtToken + }; + + } catch (error) { + console.error('Login error:', error); + return { + success: false, + message: 'Si è verificato un errore durante il login' + }; + } + } + + + async register(request: RegisterRequest): Promise { + try { + const { username, name, surname, password, mail, image } = request; + //onst userAgent = request.headers['user-agent']; + + // Validate input + this.validateRegisterInput(request); + + // Check if username already exists + const existingUser = await this.pool.query( + 'SELECT username FROM users WHERE username = $1', + [username] + ); + + if (existingUser.rows.length > 0) { + return { + success: false, + message: 'Nome utente già esistente' + }; + } + + // Check if email already exists + const existingEmail = await this.pool.query( + 'SELECT mail FROM users WHERE mail = $1', + [mail] + ); + + if (existingEmail.rows.length > 0) { + return { + success: false, + message: 'Email già esistente' + }; + } + + // Hash password + const hashedPassword = this.hashPassword(password); + + // Insert new user + const result = await this.pool.query( + 'INSERT INTO users (username, name, surname, password, mail, image, created_at) VALUES ($1, $2, $3, $4, $5, $6, NOW()) RETURNING id, username, name, surname, mail, image', + [username, name, surname, hashedPassword, mail, image || null] + ); + + const newUser = result.rows[0]; + + // Create session and get JWT token + const jwtToken = await this.createSession(newUser.id, undefined); + + return { + success: true, + message: 'Registrazione completata con successo', + user: { + id: newUser.id, + username: newUser.username, + name: newUser.name, + surname: newUser.surname, + mail: newUser.mail, + image: image + }, + token: jwtToken + }; + + } catch (error) { + console.error('Registration error:', error); + + // Handle validation errors with specific status codes + if (error instanceof Error) { + if (error.message.includes('obbligatori') || + error.message.includes('deve') || + error.message.includes('caratteri') || + error.message.includes('email valido') || + error.message.includes('contenere')) { + return { + success: false, + message: error.message + }; + } + } + + return { + success: false, + message: 'Si è verificato un errore durante la registrazione' + }; + } + } + + async validateToken(jwtToken: string): Promise { + try { + const sessionData = await this.validateSession(jwtToken); + + if (!sessionData.valid) { + return { valid: false }; + } + + return { + valid: true, + userId: sessionData.userId, + username: sessionData.username, + name: sessionData.name, + surname: sessionData.surname, + mail: sessionData.mail, + image: sessionData.image, + isAdmin: sessionData.isAdmin + }; + + } catch (error) { + console.error('Token validation error:', error); + return { valid: false }; + } + } + + async changePassword(changeData: ChangePasswordRequest): Promise { + try { + this.validateChangePasswordInput(changeData.currentPassword, changeData.newPassword); + + // Validate JWT token + const sessionData = await this.validateSession(changeData.jwtToken); + + if (!sessionData.valid) { + return { + success: false, + message: 'Sessione non valida' + }; + } + + // Get current user + const result = await this.pool.query( + 'SELECT password FROM users WHERE id = $1', + [sessionData.userId] + ); + + if (result.rows.length === 0) { + return { + success: false, + message: 'Utente non trovato' + }; + } + + // Verify current password + const isCurrentPasswordValid = this.comparePassword( + changeData.currentPassword, + result.rows[0].password + ); + + if (!isCurrentPasswordValid) { + return { + success: false, + message: 'Password corrente errata' + }; + } + + // Hash new password + const hashedNewPassword = this.hashPassword(changeData.newPassword); + + // Update password + await this.pool.query( + 'UPDATE users SET password = $1, password_updated_at = NOW() WHERE id = $2', + [hashedNewPassword, sessionData.userId] + ); + + // Invalidate all sessions for this user (force re-login) + await this.pool.query( + 'UPDATE user_sessions SET is_active = FALSE WHERE user_id = $1', + [sessionData.userId] + ); + + return { + success: true, + message: 'Password modificata con successo' + }; + + } catch (error) { + console.error('Change password error:', error); + return { + success: false, + message: 'Si è verificato un errore durante la modifica della password' + }; + } + } + + async adminChangePassword(changeData: AdminChangePasswordRequest): Promise { + try { + // Validate JWT token and check if user is admin + const sessionData = await this.validateSession(changeData.jwtToken); + + if (!sessionData.valid) { + console.error('Admin change password: Invalid session'); + return { + success: false, + message: 'Sessione non valida' + }; + } + + // Check if user is admin + if (!sessionData.isAdmin) { + console.error('Admin change password: User is not admin'); + return { + success: false, + message: 'Accesso non autorizzato. Solo gli amministratori possono modificare le password.' + }; + } + + // Validate new password + this.validateNewPassword(changeData.newPassword); + + // Check if target user exists and get their ID and email + const userResult = await this.pool.query( + 'SELECT id, username, name, surname, mail FROM users WHERE username = $1', + [changeData.userName] + ); + + if (userResult.rows.length === 0) { + console.error('Admin change password: Target user not found'); + return { + success: false, + message: 'Utente non trovato' + }; + } + + const targetUser = userResult.rows[0]; + const targetUserId = targetUser.id; + + // Hash new password + const hashedNewPassword = this.hashPassword(changeData.newPassword); + + // Update password + await this.pool.query( + 'UPDATE users SET password = $1, password_updated_at = NOW() WHERE id = $2', + [hashedNewPassword, targetUserId] + ); + + // Invalidate ALL sessions for the target user (force re-login) + const invalidateResult = await this.pool.query( + 'UPDATE user_sessions SET is_active = FALSE WHERE user_id = $1 AND is_active = TRUE', + [targetUserId] + ); + + console.log(`Invalidated ${invalidateResult.rowCount} sessions for user ${targetUser.username}`); + + // Send email notification if user has email + if (targetUser.mail && targetUser.mail.trim() !== '') { + try { + const emailService = new EmailService(); + + // Generate email template using the email templates module + const { html, text } = getPasswordResetEmailTemplate( + { + username: targetUser.username, + name: targetUser.name, + surname: targetUser.surname + }, + changeData.newPassword + ); + + await emailService.sendGmailEmail( + targetUser.mail, + 'Password Modificata - Race for Federica', + html, + text + ); + + console.log(`Password reset email sent to ${targetUser.username} (${targetUser.mail})`); + } catch (emailError) { + console.error('Failed to send password reset email:', emailError); + // Don't fail the password change if email fails + } + } + + return { + success: true, + message: `Password modificata con successo per l'utente ${targetUser.username}. Sessioni invalidate: ${invalidateResult.rowCount}.` + }; + + } catch (error) { + console.error('Admin change password error:', error); + return { + success: false, + message: 'Si è verificato un errore durante la modifica della password' + }; + } + } + + async refreshToken(oldJwtToken: string, userAgent?: string): Promise { + try { + const sessionData = await this.validateSession(oldJwtToken); + + if (!sessionData.valid) { + return { + success: false, + message: 'Sessione non valida' + }; + } + + // Generate new JWT token + const newJwtToken = this.generateJWTToken(sessionData.userId!, sessionData.username!); + + // Update session in database with new activity + await this.pool.query( + 'UPDATE user_sessions SET last_activity = NOW() WHERE user_id = $1 AND is_active = TRUE', + [sessionData.userId] + ); + + return { + success: true, + token: newJwtToken, + message: 'Token aggiornato con successo' + }; + + } catch (error) { + console.error('Refresh token error:', error); + return { + success: false, + message: 'Si è verificato un errore durante l\'aggiornamento del token' + }; + } + } + + async logout(jwtToken: string): Promise { + try { + await this.invalidateSession(jwtToken); + + return { + success: true, + message: 'Disconnessione effettuata con successo' + }; + + } catch (error) { + console.error('Logout error:', error); + return { + success: false, + message: 'Si è verificato un errore durante la disconnessione' + }; + } + } + + async logoutAllSessions(jwtToken: string): Promise { + try { + const sessionData = await this.validateSession(jwtToken); + + if (!sessionData.valid) { + return { + success: false, + message: 'Sessione non valida' + }; + } + + // Invalidate all sessions for this user + await this.pool.query( + 'UPDATE user_sessions SET is_active = FALSE WHERE user_id = $1', + [sessionData.userId] + ); + + return { + success: true, + message: 'Tutte le sessioni sono state disconnesse con successo' + }; + + } catch (error) { + console.error('Logout all sessions error:', error); + return { + success: false, + message: 'Si è verificato un errore durante la disconnessione di tutte le sessioni' + }; + } + } + + async getUserSessions(jwtToken: string): Promise { + try { + const sessionData = await this.validateSession(jwtToken); + + if (!sessionData.valid) { + return { + success: false, + message: 'Sessione non valida' + }; + } + + const result = await this.pool.query( + `SELECT session_token, created_at, last_activity, expires_at, is_active + FROM user_sessions + WHERE user_id = $1 AND is_active = TRUE + ORDER BY last_activity DESC`, + [sessionData.userId] + ); + + return { + success: true, + sessions: result.rows.map(session => ({ + sessionToken: session.session_token, + createdAt: session.created_at, + lastActivity: session.last_activity, + expiresAt: session.expires_at, + isActive: session.is_active, + isCurrent: true // Since we're using JWT, we can't easily identify the current session + })) + }; + + } catch (error) { + console.error('Get user sessions error:', error); + return { + success: false, + message: 'Si è verificato un errore durante il recupero delle sessioni' + }; + } + } + + async updateUserInfo(request: UpdateUserInfoRequest, jwt: string): Promise { + try { + const { name, surname, mail, image } = request; + + if (!jwt) { + return { + success: false, + message: 'Token JWT è obbligatorio' + }; + } + + // Validate JWT token + const sessionData = await this.validateSession(jwt); + + if (!sessionData.valid) { + return { + success: false, + message: 'Sessione non valida' + }; + } + + // Build updates object from provided fields + const updates = { + name, + surname, + mail, + image + }; + + // Validate updates + this.validateUpdateUserInfoInput(updates); + + // If email is being updated, check if it already exists + if (mail) { + const existingEmail = await this.pool.query( + 'SELECT id FROM users WHERE mail = $1 AND id != $2', + [mail, sessionData.userId] + ); + + if (existingEmail.rows.length > 0) { + return { + success: false, + message: 'Email già esistente' + }; + } + } + + // Build dynamic query + const updateFields: string[] = []; + const values: any[] = []; + let parameterIndex = 1; + + if (name !== undefined) { + updateFields.push(`name = $${parameterIndex}`); + values.push(name); + parameterIndex++; + } + + if (surname !== undefined) { + updateFields.push(`surname = $${parameterIndex}`); + values.push(surname); + parameterIndex++; + } + + if (mail !== undefined) { + updateFields.push(`mail = $${parameterIndex}`); + values.push(mail); + parameterIndex++; + } + + if (image !== undefined) { + updateFields.push(`image = $${parameterIndex}`); + values.push(image); + parameterIndex++; + } + + // If no fields to update + if (updateFields.length === 0) { + return { + success: false, + message: 'Nessun campo valido da aggiornare' + }; + } + + // Add user ID to values + values.push(sessionData.userId); + + // Execute update query + const query = ` + UPDATE users + SET ${updateFields.join(', ')} + WHERE id = $${parameterIndex} + RETURNING id, username, name, surname, mail, encode(image, 'escape') as image, is_admin + `; + + const result = await this.pool.query(query, values); + + if (result.rows.length === 0) { + return { + success: false, + message: 'Utente non trovato' + }; + } + + const updatedUser = result.rows[0]; + + return { + success: true, + message: 'Informazioni utente aggiornate con successo', + user: { + id: updatedUser.id, + username: updatedUser.username, + name: updatedUser.name, + surname: updatedUser.surname, + mail: updatedUser.mail, + image: updatedUser.image, + isAdmin: updatedUser.is_admin + } + }; + + } catch (error) { + console.error('Update user info error:', error); + + // Handle validation errors + if (error instanceof Error) { + if (error.message.includes('obbligatori') || + error.message.includes('deve') || + error.message.includes('caratteri') || + error.message.includes('email valido') || + error.message.includes('contenere')) { + return { + success: false, + message: error.message + }; + } + } + + return { + success: false, + message: 'Si è verificato un errore durante l\'aggiornamento delle informazioni utente' + }; + } + } + + private hashPassword(password: string): string { + return createHash('sha256').update(password).digest('hex'); + } + + private comparePassword(password: string, hashedPassword: string): boolean { + return this.hashPassword(password) === hashedPassword; + } + + private sanitizeUserAgent(userAgent?: string): string | undefined { + if (!userAgent) return undefined; + + // Limit user agent length to prevent database issues + const maxLength = 500; + return userAgent.length > maxLength ? userAgent.substring(0, maxLength) : userAgent; + } + + private generateSessionToken(): string { + return randomBytes(32).toString('hex'); + } + + private generateJWTToken(userId: number, username: string, isAdmin: boolean = false): string { + return jwt.sign( + { + userId: userId, + username: username, + isAdmin: isAdmin + }, + this.jwtSecret, + { expiresIn: AuthService.TOKEN_EXPIRY_JWT } + ); + } + + private async createSession(userId: number, userAgent?: string): Promise { + const sessionToken = this.generateSessionToken(); + const expiresAt = new Date(); + expiresAt.setDate(expiresAt.getDate() + AuthService.TOKEN_EXPIRY_DAYS); // 7 days from now + + // Generate JWT token + const user = await this.pool.query( + 'SELECT username, is_admin FROM users WHERE id = $1', + [userId] + ); + + const jwtToken = this.generateJWTToken(userId, user.rows[0].username, user.rows[0].is_admin); + + // Sanitize user agent for database storage + const sanitizedUserAgent = this.sanitizeUserAgent(userAgent); + + await this.pool.query( + `INSERT INTO user_sessions (user_id, session_token, expires_at, ip_address, user_agent) + VALUES ($1, $2, $3, $4, $5)`, + [userId, sessionToken, expiresAt, null, sanitizedUserAgent || null] + ); + + return jwtToken; + } + + private async validateSession(jwtToken: string): Promise<{ valid: boolean; userId?: number; username?: string; name?: string; surname?: string; mail?: string; image?: string; isAdmin?: boolean }> { + try { + // Verify JWT token + const decoded = jwt.verify(jwtToken, this.jwtSecret) as any; + + // Check if user still exists and is active + const result = await this.pool.query( + 'SELECT id, username, is_active, name, surname, mail, encode(image, \'escape\') as image, is_admin FROM users WHERE id = $1', + [decoded.userId] + ); + + if (result.rows.length === 0) { + return { valid: false }; + } + + const user = result.rows[0]; + + // Check if user account is active + if (!user.is_active) { + return { valid: false }; + } + + // Check if there's an active session for this user + const sessionResult = await this.pool.query( + `SELECT session_token, expires_at, is_active + FROM user_sessions + WHERE user_id = $1 AND is_active = TRUE + ORDER BY last_activity DESC + LIMIT 1`, + [decoded.userId] + ); + + if (sessionResult.rows.length === 0) { + return { valid: false }; + } + + const session = sessionResult.rows[0]; + + // Check if session is expired + if (new Date() > new Date(session.expires_at)) { + // Clean up expired session + await this.pool.query( + 'UPDATE user_sessions SET is_active = FALSE WHERE user_id = $1', + [decoded.userId] + ); + return { valid: false }; + } + + // Update last activity for the most recent session + await this.pool.query( + 'UPDATE user_sessions SET last_activity = NOW() WHERE session_token = $1', + [session.session_token] + ); + + return { + valid: true, + userId: user.id, + username: user.username, + name: user.name, + surname: user.surname, + mail: user.mail, + image: user.image, + isAdmin: user.is_admin + }; + + } catch (error) { + console.error('Token validation error:', error); + return { valid: false }; + } + } + + private async invalidateSession(jwtToken: string): Promise { + try { + // Decode JWT to get user ID + const decoded = jwt.verify(jwtToken, this.jwtSecret) as any; + + // Invalidate all sessions for this user + await this.pool.query( + 'UPDATE user_sessions SET is_active = FALSE WHERE user_id = $1', + [decoded.userId] + ); + } catch (error) { + console.error('Error invalidating session:', error); + } + } + + private async cleanExpiredSessions(): Promise { + // Mark expired sessions as inactive + await this.pool.query( + 'UPDATE user_sessions SET is_active = FALSE WHERE expires_at < NOW() AND is_active = TRUE' + ); + + // Delete very old sessions (older than 30 days) to prevent database bloat + await this.pool.query( + 'DELETE FROM user_sessions WHERE created_at < NOW() - INTERVAL \'30 days\'' + ); + } + + private validateLoginInput(username: string, password: string): void { + if (!username || !password) { + throw new Error('Nome utente e password sono obbligatori'); + } + + if (typeof username !== 'string' || typeof password !== 'string') { + throw new Error('Nome utente e password devono essere stringhe'); + } + } + + private validateRegisterInput(registerData: RegisterRequest): void { + const { username, name, surname, password, mail, image } = registerData; + + if (!username || !name || !surname || !password || !mail || !image) { + throw new Error('Tutti i campi sono obbligatori inclusa l\'immagine del profilo'); + } + + if (username.length < 3 || username.length > 30) { + throw new Error('Il nome utente deve contenere tra 3 e 30 caratteri'); + } + + if (password.length < 8) { + throw new Error('La password deve contenere almeno 8 caratteri'); + } + + // Email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(mail)) { + throw new Error('Inserire un indirizzo email valido'); + } + + // Image validation + if (image.length > 5000000) { // 5MB limit + throw new Error('Dimensione immagine troppo grande (massimo 5MB)'); + } + + // Check if image is a valid base64 string + const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/; + let cleanImage = image; + + // Remove data URL prefix if present + if (image.startsWith('data:')) { + const commaIndex = image.indexOf(','); + if (commaIndex !== -1) { + cleanImage = image.substring(commaIndex + 1); + } + } + + if (!base64Regex.test(cleanImage)) { + throw new Error('Formato immagine non valido. Fornire un\'immagine codificata in base64 valida'); + } + + // Password strength validation + const hasUpperCase = /[A-Z]/.test(password); + const hasLowerCase = /[a-z]/.test(password); + const hasNumbers = /\d/.test(password); + + if (!hasUpperCase || !hasLowerCase || !hasNumbers) { + throw new Error('La password deve contenere almeno una lettera maiuscola, una lettera minuscola e un numero'); + } + + // Username format validation + const usernameRegex = /^[a-zA-Z0-9_]+$/; + if (!usernameRegex.test(username)) { + throw new Error('Il nome utente può contenere solo lettere, numeri e underscore'); + } + } + + private validateChangePasswordInput(currentPassword: string, newPassword: string): void { + if (!currentPassword || !newPassword) { + throw new Error('Password corrente e nuova password sono obbligatorie'); + } + + if (newPassword.length < 8) { + throw new Error('La nuova password deve contenere almeno 8 caratteri'); + } + + if (currentPassword === newPassword) { + throw new Error('La nuova password deve essere diversa dalla password corrente'); + } + + // Password strength validation for new password + const hasUpperCase = /[A-Z]/.test(newPassword); + const hasLowerCase = /[a-z]/.test(newPassword); + const hasNumbers = /\d/.test(newPassword); + + if (!hasUpperCase || !hasLowerCase || !hasNumbers) { + throw new Error('La nuova password deve contenere almeno una lettera maiuscola, una lettera minuscola e un numero'); + } + } + + private validateNewPassword(newPassword: string): void { + if (!newPassword) { + throw new Error('La nuova password è obbligatoria'); + } + + if (newPassword.length < 8) { + throw new Error('La nuova password deve contenere almeno 8 caratteri'); + } + } + + private validateUpdateUserInfoInput(updates: { + name?: string; + surname?: string; + mail?: string; + image?: string; + }): void { + // Check if at least one field is provided + if (!updates || Object.keys(updates).length === 0) { + throw new Error('At least one field must be provided for update'); + } + + // Validate name if provided + if (updates.name !== undefined) { + if (typeof updates.name !== 'string') { + throw new Error('Name must be a string'); + } + if (updates.name.trim().length === 0) { + throw new Error('Name cannot be empty'); + } + if (updates.name.length > 50) { + throw new Error('Name must be 50 characters or less'); + } + } + + // Validate surname if provided + if (updates.surname !== undefined) { + if (typeof updates.surname !== 'string') { + throw new Error('Surname must be a string'); + } + if (updates.surname.trim().length === 0) { + throw new Error('Surname cannot be empty'); + } + if (updates.surname.length > 50) { + throw new Error('Surname must be 50 characters or less'); + } + } + + // Validate email if provided + if (updates.mail !== undefined) { + if (typeof updates.mail !== 'string') { + throw new Error('Email must be a string'); + } + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(updates.mail)) { + throw new Error('Please enter a valid email address'); + } + } + + // Validate image if provided + if (updates.image !== undefined) { + if (typeof updates.image !== 'string') { + throw new Error('Image must be a string'); + } + + if (updates.image.length > 5000000) { // 5MB limit + throw new Error('Image size is too large (max 5MB)'); + } + + // Check if image is a valid base64 string + const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/; + let cleanImage = updates.image; + + // Remove data URL prefix if present + if (updates.image.startsWith('data:')) { + const commaIndex = updates.image.indexOf(','); + if (commaIndex !== -1) { + cleanImage = updates.image.substring(commaIndex + 1); + } + } + + if (!base64Regex.test(cleanImage)) { + throw new Error('Invalid image format. Please provide a valid base64 encoded image'); + } + } + } +} diff --git a/server/src/services/database.service.ts b/server/src/services/database.service.ts new file mode 100644 index 000000000..82c3ec512 --- /dev/null +++ b/server/src/services/database.service.ts @@ -0,0 +1,560 @@ +import pg from 'pg'; +import type { + DriverData, + Driver, + SessionResult, + ChampionshipData, + Season, + CumulativePointsData, + TrackData, + RaceResult, + Constructor, + ConstructorGrandPrixPoints +} from '@f123dashboard/shared'; + +export class DatabaseService { + constructor(private pool: pg.Pool) {} + + async getAllDrivers(seasonId?: number): Promise { + const result = await this.pool.query(` + WITH latest_season AS ( + SELECT id FROM seasons ORDER BY start_date DESC LIMIT 1 + ) + SELECT + driver_id, driver_username, driver_name, driver_surname, driver_description, driver_license_pt, driver_consistency_pt, driver_fast_lap_pt, drivers_dangerous_pt, driver_ingenuity_pt, driver_strategy_pt, driver_color, car_name, car_overall_score, total_sprint_points, total_free_practice_points, total_qualifying_points, total_full_race_points, total_race_points, total_points + FROM public.all_race_points arp + CROSS JOIN latest_season ls + WHERE arp.season_id = COALESCE($1, ls.id); + `, [seasonId]); + return result.rows as DriverData[]; + } + + async getDriversData(seasonId?: number): Promise { + const result = await this.pool.query(` + WITH latest_season AS ( + SELECT id FROM seasons ORDER BY start_date DESC LIMIT 1 + ) + SELECT + d.id as id, + d.username as username, + d.name as name, + d.surname as surname + FROM drivers d + CROSS JOIN latest_season ls + LEFT JOIN pilots p ON d.pilot_id = p.id + LEFT JOIN cars c ON p.car_id = c.id + WHERE d.season = COALESCE($1, ls.id) + ORDER BY d.username; + `, [seasonId]); + return result.rows as Driver[]; + } + + async getChampionship(seasonId?: number): Promise { + const result = await this.pool.query(` + WITH latest_season AS ( + SELECT id FROM seasons ORDER BY start_date DESC LIMIT 1 + ), + all_session_results AS ( + -- Free Practice Results + SELECT + gp.id AS gran_prix_id, + t.name AS track_name, + gp.date AS gran_prix_date, + gp.has_sprint AS gran_prix_has_sprint, + gp.has_x2 AS gran_prix_has_x2, + t.country AS track_country, + 'free_practice' AS session_type, + fpre.position, + d.username AS driver_username, + NULL::boolean AS fast_lap + FROM gran_prix gp + JOIN tracks t ON gp.track_id = t.id + CROSS JOIN latest_season ls + LEFT JOIN free_practice_result_entries fpre ON fpre.free_practice_results_id = gp.free_practice_results_id + LEFT JOIN drivers d ON fpre.pilot_id = d.id + WHERE gp.season_id = COALESCE($1, ls.id) + AND gp.free_practice_results_id IS NOT NULL + AND fpre.position IS NOT NULL + + UNION ALL + + -- Qualifying Results + SELECT + gp.id AS gran_prix_id, + t.name AS track_name, + gp.date AS gran_prix_date, + gp.has_sprint AS gran_prix_has_sprint, + gp.has_x2 AS gran_prix_has_x2, + t.country AS track_country, + 'qualifying' AS session_type, + qre.position, + d.username AS driver_username, + NULL::boolean AS fast_lap + FROM gran_prix gp + JOIN tracks t ON gp.track_id = t.id + CROSS JOIN latest_season ls + LEFT JOIN qualifying_result_entries qre ON qre.qualifying_results_id = gp.qualifying_results_id + LEFT JOIN drivers d ON qre.pilot_id = d.id + WHERE gp.season_id = COALESCE($1, ls.id) + AND gp.qualifying_results_id IS NOT NULL + AND qre.position IS NOT NULL + + UNION ALL + + -- Race Results + SELECT + gp.id AS gran_prix_id, + t.name AS track_name, + gp.date AS gran_prix_date, + gp.has_sprint AS gran_prix_has_sprint, + gp.has_x2 AS gran_prix_has_x2, + t.country AS track_country, + 'race' AS session_type, + rre.position, + d.username AS driver_username, + rre.fast_lap + FROM gran_prix gp + JOIN tracks t ON gp.track_id = t.id + CROSS JOIN latest_season ls + LEFT JOIN race_result_entries rre ON rre.race_results_id = gp.race_results_id + LEFT JOIN drivers d ON rre.pilot_id = d.id + WHERE gp.season_id = COALESCE($1, ls.id) + AND gp.race_results_id IS NOT NULL + AND rre.position IS NOT NULL + + UNION ALL + + -- Sprint Results + SELECT + gp.id AS gran_prix_id, + t.name AS track_name, + gp.date AS gran_prix_date, + gp.has_sprint AS gran_prix_has_sprint, + gp.has_x2 AS gran_prix_has_x2, + t.country AS track_country, + 'sprint' AS session_type, + sre.position, + d.username AS driver_username, + sre.fast_lap + FROM gran_prix gp + JOIN tracks t ON gp.track_id = t.id + CROSS JOIN latest_season ls + LEFT JOIN sprint_result_entries sre ON sre.sprint_results_id = gp.sprint_results_id + LEFT JOIN drivers d ON sre.pilot_id = d.id + WHERE gp.season_id = COALESCE($1, ls.id) + AND gp.sprint_results_id IS NOT NULL + AND sre.position IS NOT NULL + + UNION ALL + + -- Full Race Results + SELECT + gp.id AS gran_prix_id, + t.name AS track_name, + gp.date AS gran_prix_date, + gp.has_sprint AS gran_prix_has_sprint, + gp.has_x2 AS gran_prix_has_x2, + t.country AS track_country, + 'full_race' AS session_type, + frre.position, + d.username AS driver_username, + frre.fast_lap + FROM gran_prix gp + JOIN tracks t ON gp.track_id = t.id + CROSS JOIN latest_season ls + LEFT JOIN full_race_result_entries frre ON frre.race_results_id = gp.full_race_results_id + LEFT JOIN drivers d ON frre.pilot_id = d.id + WHERE gp.season_id = COALESCE($1, ls.id) + AND gp.full_race_results_id IS NOT NULL + AND frre.position IS NOT NULL + ), + grand_prix_base AS ( + SELECT DISTINCT + gran_prix_id, + track_name, + gran_prix_date, + gran_prix_has_sprint, + gran_prix_has_x2, + track_country + FROM all_session_results + + UNION + + -- Include Grand Prix without results + SELECT + gp.id AS gran_prix_id, + t.name AS track_name, + gp.date AS gran_prix_date, + gp.has_sprint AS gran_prix_has_sprint, + gp.has_x2 AS gran_prix_has_x2, + t.country AS track_country + FROM gran_prix gp + JOIN tracks t ON gp.track_id = t.id + CROSS JOIN latest_season ls + WHERE gp.season_id = COALESCE($1, ls.id) + ) + SELECT + gpb.gran_prix_id, + gpb.track_name, + gpb.gran_prix_date, + gpb.gran_prix_has_sprint, + gpb.gran_prix_has_x2, + gpb.track_country, + asr.session_type, + asr.position, + asr.driver_username, + asr.fast_lap + FROM grand_prix_base gpb + LEFT JOIN all_session_results asr ON gpb.gran_prix_id = asr.gran_prix_id + ORDER BY gpb.gran_prix_date ASC, asr.session_type, asr.position; + `, [seasonId]); + + // Process results into the required format + const champMap = new Map(); + + for (const row of result.rows) { + const gpId = row.gran_prix_id.toString(); + + if (!champMap.has(gpId)) { + champMap.set(gpId, { + gran_prix_id: gpId, + track_name: row.track_name, + gran_prix_date: row.gran_prix_date, + gran_prix_has_sprint: Number(row.gran_prix_has_sprint) || 0, + gran_prix_has_x2: Number(row.gran_prix_has_x2) || 0, + track_country: row.track_country, + sessions: {}, + fastLapDrivers: {} + }); + } + + const champData = champMap.get(gpId); + + if (row.session_type && row.driver_username) { + if (!champData.sessions[row.session_type]) { + champData.sessions[row.session_type] = []; + } + + champData.sessions[row.session_type].push({ + position: row.position, + driver_username: row.driver_username, + fast_lap: row.fast_lap + }); + + if (row.fast_lap === true) { + champData.fastLapDrivers[row.session_type] = row.driver_username; + } + } + } + + const formattedResults = Array.from(champMap.values()) + .sort((a, b) => new Date(a.gran_prix_date).getTime() - new Date(b.gran_prix_date).getTime()); + + return formattedResults as ChampionshipData[]; + } + + async getCumulativePoints(seasonId?: number): Promise { + const result = await this.pool.query(` + WITH latest_season AS ( + SELECT id + FROM seasons + ORDER BY start_date DESC + LIMIT 1 + ), + all_session_points AS ( + SELECT + dgp.grand_prix_date AS date, + dgp.track_name, + dgp.pilot_id AS driver_id, + dgp.pilot_username AS driver_username, + dgp.pilot_id, + dgp.position_points + dgp.fast_lap_points AS session_point + FROM public.driver_grand_prix_points dgp + CROSS JOIN latest_season ls + WHERE dgp.season_id = COALESCE($1, ls.id) + ), + driver_colors AS ( + SELECT id AS driver_id, color AS driver_color + FROM drivers + ) + SELECT + asp.date, + asp.track_name, + asp.driver_id, + asp.driver_username, + dc.driver_color, + SUM(asp.session_point) OVER (PARTITION BY asp.driver_id ORDER BY asp.date, asp.track_name) AS cumulative_points + FROM all_session_points asp + LEFT JOIN driver_colors dc ON asp.driver_id = dc.driver_id + GROUP BY asp.date, asp.track_name, asp.driver_id, asp.driver_username, dc.driver_color, asp.session_point + ORDER BY asp.driver_id, asp.date, asp.track_name; + `, [seasonId]); + return result.rows as CumulativePointsData[]; + } + + async getAllTracks(seasonId?: number): Promise { + const result = await this.pool.query(` + WITH latest_season AS ( + SELECT id + FROM seasons + ORDER BY start_date DESC + LIMIT 1 + ) + SELECT outer_table_tracks.track_id, outer_table_tracks.name, outer_table_tracks.date, outer_table_tracks.has_sprint, outer_table_tracks.has_x2, outer_table_tracks.country, outer_table_tracks.besttime_driver_time, + outer_table_drivers.username + FROM + ( + SELECT * + FROM tracks + LEFT JOIN + ( + SELECT * + FROM gran_prix + CROSS JOIN latest_season ls + WHERE gran_prix.season_id = COALESCE($1, ls.id) + ) AS inner_table + ON tracks.id = inner_table.track_id + ) AS outer_table_tracks + LEFT JOIN + ( + SELECT * + FROM drivers + ) AS outer_table_drivers + ON outer_table_tracks.besttime_driver_id = outer_table_drivers.id + WHERE outer_table_tracks.date IS NOT NULL + ORDER BY date ASC + `, [seasonId]); + return result.rows as TrackData[]; + } + + async getRaceResult(seasonId?: number): Promise { + const result = await this.pool.query(` + WITH latest_season AS ( + SELECT id + FROM seasons + ORDER BY start_date DESC + LIMIT 1 + ) + SELECT + gp.id AS id, + gp.track_id AS track_id, + MAX(CASE WHEN rre.position = 1 THEN rre.pilot_id END) AS id_1_place, + MAX(CASE WHEN rre.position = 2 THEN rre.pilot_id END) AS id_2_place, + MAX(CASE WHEN rre.position = 3 THEN rre.pilot_id END) AS id_3_place, + MAX(CASE WHEN rre.position = 4 THEN rre.pilot_id END) AS id_4_place, + MAX(CASE WHEN rre.position = 5 THEN rre.pilot_id END) AS id_5_place, + MAX(CASE WHEN rre.position = 6 THEN rre.pilot_id END) AS id_6_place, + MAX(CASE WHEN rre.position = 7 THEN rre.pilot_id END) AS id_7_place, + MAX(CASE WHEN rre.position = 8 THEN rre.pilot_id END) AS id_8_place, + MAX(CASE WHEN rre.fast_lap THEN rre.pilot_id END) AS id_fast_lap, + ARRAY_AGG(rre.pilot_id) FILTER (WHERE rre.position = 0) AS list_dnf + FROM gran_prix gp + CROSS JOIN latest_season ls + LEFT JOIN race_result_entries rre ON gp.race_results_id = rre.race_results_id + WHERE gp.race_results_id IS NOT NULL + AND gp.season_id = COALESCE($1, ls.id) + GROUP BY gp.id + + UNION ALL + + SELECT + gp.id AS track_id, + gp.track_id AS track_id, + MAX(CASE WHEN frre.position = 1 THEN frre.pilot_id END) AS id_1_place, + MAX(CASE WHEN frre.position = 2 THEN frre.pilot_id END) AS id_2_place, + MAX(CASE WHEN frre.position = 3 THEN frre.pilot_id END) AS id_3_place, + MAX(CASE WHEN frre.position = 4 THEN frre.pilot_id END) AS id_4_place, + MAX(CASE WHEN frre.position = 5 THEN frre.pilot_id END) AS id_5_place, + MAX(CASE WHEN frre.position = 6 THEN frre.pilot_id END) AS id_6_place, + MAX(CASE WHEN frre.position = 7 then frre.pilot_id END) AS id_7_place, + MAX(CASE WHEN frre.position = 8 THEN frre.pilot_id END) AS id_8_place, + MAX(CASE WHEN frre.fast_lap THEN frre.pilot_id END) AS id_fast_lap, + ARRAY_AGG(frre.pilot_id) FILTER (WHERE frre.position = 0) AS list_dnf + FROM gran_prix gp + CROSS JOIN latest_season ls + LEFT JOIN full_race_result_entries frre ON gp.full_race_results_id = frre.race_results_id + WHERE gp.full_race_results_id IS NOT NULL + AND gp.season_id = COALESCE($1, ls.id) + GROUP BY gp.id + `, [seasonId]); + return result.rows as RaceResult[]; + } + + async getAllSeasons(): Promise { + const result = await this.pool.query(`SELECT id, description, start_date, end_date FROM seasons ORDER BY id DESC`); + return result.rows as Season[]; + } + + async getConstructors(seasonId?: number): Promise { + const result = await this.pool.query(` + SELECT constructor_id, + constructor_name, + constructor_color, + driver_1_id, + driver_1_username, + driver_1_tot_points, + driver_2_id, + driver_2_username, + driver_2_tot_points, + constructor_tot_points + FROM season_constructor_leaderboard + `); + return result.rows as Constructor[]; + } + + async getConstructorGrandPrixPoints(seasonId?: number): Promise { + const result = await this.pool.query(` + WITH latest_season AS ( + SELECT id FROM seasons ORDER BY start_date DESC LIMIT 1 + ) + SELECT + constructor_id, + constructor_name, + grand_prix_id, + grand_prix_date, + track_name, + track_id, + season_id, + season_description, + driver_id_1, + driver_id_2, + driver_1_points, + driver_2_points, + constructor_points + FROM constructor_grand_prix_points cgp + CROSS JOIN latest_season ls + WHERE cgp.season_id = COALESCE($1, ls.id) + ORDER BY cgp.grand_prix_date DESC, cgp.constructor_points DESC; + `, [seasonId]); + return result.rows as ConstructorGrandPrixPoints[]; + } + + async setGpResult( + trackId: number, + hasSprint: boolean, + raceResult: number[], + raceDnfResult: number[], + sprintResult: number[], + sprintDnfResult: number[], + qualiResult: number[], + fpResult: number[], + seasonId: number + ): Promise<{ success: boolean; message: string }> { + const client = await this.pool.connect(); + try { + await client.query('BEGIN'); + + const gpRes = await client.query( + 'SELECT id, race_results_id, sprint_results_id, qualifying_results_id, free_practice_results_id, full_race_results_id, has_x2 FROM gran_prix WHERE track_id = $1 and season_id = $2', + [trackId, seasonId] + ); + + if (gpRes.rowCount === 0) throw new Error('Gran Prix not found'); + const gp = gpRes.rows[0]; + const hasX2Enabled = Number(gp.has_x2) === 1; + const raceFastLapPilotId = raceResult[8]; + const sprintFastLapPilotId = sprintResult[8]; + + // Handle Race or Full Race Results + if (hasX2Enabled && gp.full_race_results_id) { + await client.query('DELETE FROM full_race_result_entries WHERE race_results_id = $1', [gp.full_race_results_id]); + for (let i = 0; i < 8; i++) { + if (raceResult[i] && raceResult[i] !== 0) { + await client.query( + 'INSERT INTO full_race_result_entries (race_results_id, pilot_id, position, fast_lap) VALUES ($1, $2, $3, $4)', + [gp.full_race_results_id, raceResult[i], i+1, raceFastLapPilotId === raceResult[i]] + ); + } + } + if (raceDnfResult && raceDnfResult.length > 0) { + for (const pilotId of raceDnfResult) { + await client.query( + 'INSERT INTO full_race_result_entries (race_results_id, pilot_id, position, fast_lap) VALUES ($1, $2, $3, $4)', + [gp.full_race_results_id, pilotId, 0, false] + ); + } + } + } else if (gp.race_results_id) { + await client.query('DELETE FROM race_result_entries WHERE race_results_id = $1', [gp.race_results_id]); + for (let i = 0; i < 8; i++) { + if (raceResult[i] && raceResult[i] !== 0) { + await client.query( + 'INSERT INTO race_result_entries (race_results_id, pilot_id, position, fast_lap) VALUES ($1, $2, $3, $4)', + [gp.race_results_id, raceResult[i], i+1, raceFastLapPilotId === raceResult[i]] + ); + } + } + if (raceDnfResult && raceDnfResult.length > 0) { + for (const pilotId of raceDnfResult) { + await client.query( + 'INSERT INTO race_result_entries (race_results_id, pilot_id, position, fast_lap) VALUES ($1, $2, $3, $4)', + [gp.race_results_id, pilotId, 0, false] + ); + } + } + } + + // Handle Sprint Results + if (hasSprint && gp.sprint_results_id) { + await client.query('DELETE FROM sprint_result_entries WHERE sprint_results_id = $1', [gp.sprint_results_id]); + for (let i = 0; i < 8; i++) { + if (sprintResult[i] && sprintResult[i] !== 0) { + await client.query( + 'INSERT INTO sprint_result_entries (sprint_results_id, pilot_id, position, fast_lap) VALUES ($1, $2, $3, $4)', + [gp.sprint_results_id, sprintResult[i], i+1, sprintFastLapPilotId === sprintResult[i]] + ); + } + } + if (sprintDnfResult && sprintDnfResult.length > 0) { + for (const pilotId of sprintDnfResult) { + await client.query( + 'INSERT INTO sprint_result_entries (sprint_results_id, pilot_id, position, fast_lap) VALUES ($1, $2, $3, $4)', + [gp.sprint_results_id, pilotId, 0, false] + ); + } + } + } + + // Handle Qualifying Results + if (gp.qualifying_results_id) { + await client.query('DELETE FROM qualifying_result_entries WHERE qualifying_results_id = $1', [gp.qualifying_results_id]); + for (let i = 0; i < 8; i++) { + if (qualiResult[i] && qualiResult[i] !== 0) { + await client.query( + 'INSERT INTO qualifying_result_entries (qualifying_results_id, pilot_id, position) VALUES ($1, $2, $3)', + [gp.qualifying_results_id, qualiResult[i], i+1] + ); + } + } + } + + // Handle Free Practice Results + if (gp.free_practice_results_id) { + await client.query('DELETE FROM free_practice_result_entries WHERE free_practice_results_id = $1', [gp.free_practice_results_id]); + for (let i = 0; i < 8; i++) { + if (fpResult[i] && fpResult[i] !== 0) { + await client.query( + 'INSERT INTO free_practice_result_entries (free_practice_results_id, pilot_id, position) VALUES ($1, $2, $3)', + [gp.free_practice_results_id, fpResult[i], i+1] + ); + } + } + } + + await client.query('COMMIT'); + return { + success: true, + message: 'Race result saved successfully' + }; + } catch (error) { + await client.query('ROLLBACK'); + console.error('[setGpResult] Error saving race result:', error); + return { + success: false, + message: `Failed to save race result: ${error instanceof Error ? error.message : 'Unknown error'}` + }; + } finally { + client.release(); + } + } +} diff --git a/server/src/services/fanta.service.ts b/server/src/services/fanta.service.ts new file mode 100644 index 000000000..549081564 --- /dev/null +++ b/server/src/services/fanta.service.ts @@ -0,0 +1,120 @@ +import pg from "pg"; +import type { FantaVote } from '@f123dashboard/shared'; + +export class FantaService { + constructor(private pool: pg.Pool) {} + + async getFantaVote(seasonId?: number): Promise { + const result = await this.pool.query(` + WITH latest_season AS ( + SELECT id FROM seasons ORDER BY start_date DESC LIMIT 1 + ) + SELECT + fp_table.id AS fanta_player_id, + f_table.race_id AS track_id, + fp_table.username AS username, + f_table."1_place_id" AS "id_1_place", + f_table."2_place_id" AS "id_2_place", + f_table."3_place_id" AS "id_3_place", + f_table."4_place_id" AS "id_4_place", + f_table."5_place_id" AS "id_5_place", + f_table."6_place_id" AS "id_6_place", + f_table."7_place_id" AS "id_7_place", + f_table."8_place_id" AS "id_8_place", + f_table."fast_lap_id" AS "id_fast_lap", + f_table."dnf_id" AS "id_dnf", + f_table."team_id" AS "constructor_id" + FROM users fp_table + JOIN fanta f_table ON fp_table.id = f_table.fanta_player_id + CROSS JOIN latest_season ls + WHERE f_table.season_id = COALESCE($1, ls.id) + ORDER BY fp_table.id, f_table.race_id; + `, [seasonId]); + return result.rows as FantaVote[]; + } + + async setFantaVoto(fantaVote: FantaVote): Promise<{ success: boolean; message: string }> { + try { + // Validate input + this.validateFantaVoto(fantaVote); + + // Get the season_id (use provided or get latest) + let season_id = fantaVote.season_id; + if (season_id == null || season_id == undefined) { + const seasonResult = await this.pool.query('SELECT id FROM seasons ORDER BY start_date DESC LIMIT 1'); + season_id = seasonResult.rows[0]?.id; + } + + const query = ` + INSERT INTO "fanta" ( + "fanta_player_id", "race_id", "1_place_id", "2_place_id", "3_place_id", + "4_place_id", "5_place_id", "6_place_id", "7_place_id", "8_place_id", + "fast_lap_id", "dnf_id", "season_id", "team_id" + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) + ON CONFLICT ("fanta_player_id", "race_id", "season_id") + DO UPDATE SET + "1_place_id" = EXCLUDED."1_place_id", + "2_place_id" = EXCLUDED."2_place_id", + "3_place_id" = EXCLUDED."3_place_id", + "4_place_id" = EXCLUDED."4_place_id", + "5_place_id" = EXCLUDED."5_place_id", + "6_place_id" = EXCLUDED."6_place_id", + "7_place_id" = EXCLUDED."7_place_id", + "8_place_id" = EXCLUDED."8_place_id", + "fast_lap_id" = EXCLUDED."fast_lap_id", + "dnf_id" = EXCLUDED."dnf_id", + "season_id" = EXCLUDED."season_id", + "team_id" = EXCLUDED."team_id" + `; + + const values = [ + fantaVote.fanta_player_id, + fantaVote.track_id, + fantaVote.id_1_place, + fantaVote.id_2_place, + fantaVote.id_3_place, + fantaVote.id_4_place, + fantaVote.id_5_place, + fantaVote.id_6_place, + fantaVote.id_7_place, + fantaVote.id_8_place, + fantaVote.id_fast_lap, + fantaVote.id_dnf, + season_id, + fantaVote.constructor_id + ]; + + await this.pool.query(query, values); + + console.log(`Successfully saved fanta vote for player ${fantaVote.fanta_player_id} on race ${fantaVote.track_id} for season ${fantaVote.season_id}`); + + return { + success: true, + message: 'Fanta vote saved successfully' + }; + } catch (error) { + console.error('Error saving fanta vote:', error); + throw new Error(`Failed to save fanta vote: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + + private validateFantaVoto(fantavote: FantaVote): void { + // Validate required fields + if (!fantavote.fanta_player_id || !fantavote.track_id) throw new Error('Fanta player ID and track ID are required'); + if (!fantavote.id_fast_lap) throw new Error('Fast lap driver ID is required'); + if (!fantavote.id_dnf) throw new Error('DNF driver ID is required'); + if (!fantavote.constructor_id) throw new Error("Constructor ID is required"); + + // Validate that all 8 places are different (if provided) + const places = [ + fantavote.id_1_place, fantavote.id_2_place, fantavote.id_3_place, fantavote.id_4_place, + fantavote.id_5_place, fantavote.id_6_place, fantavote.id_7_place, fantavote.id_8_place + ].filter(place => place !== null && place !== undefined); + + const uniquePlaces = new Set(places); + if (places.length !== uniquePlaces.size) { + throw new Error('All driver positions must be unique'); + } + } +} diff --git a/server/src/services/gp-edit.service.ts b/server/src/services/gp-edit.service.ts new file mode 100644 index 000000000..dae13f731 --- /dev/null +++ b/server/src/services/gp-edit.service.ts @@ -0,0 +1,151 @@ +import pg from 'pg'; +import type { GPEditItem, CreateGpData, UpdateGpData } from '@f123dashboard/shared'; + +export class GpEditService { + constructor(private pool: pg.Pool) {} + +async getUpcomingGps(): Promise { + // Get the greatest (latest) season_id + const seasonRes = await this.pool.query( + `SELECT id FROM seasons ORDER BY start_date DESC LIMIT 1` + ); + if (seasonRes.rows.length === 0) { + return []; + } + const seasonId = seasonRes.rows[0].id; + + const query = ` + SELECT + gp.id, + gp.date, + gp.track_id, + t.name as track_name, + gp.has_sprint, + gp.has_x2 + FROM gran_prix gp + JOIN tracks t ON gp.track_id = t.id + WHERE gp.season_id = $1 + AND gp.date > NOW() + ORDER BY gp.date ASC + `; + const result = await this.pool.query(query, [seasonId]); + return result.rows.map(row => ({ + ...row, + has_sprint: row.has_sprint == 1, + has_x2: row.has_x2 == 1 + })); +} + + async createGp(data: CreateGpData): Promise { + const seasonQuery = `SELECT id FROM seasons ORDER BY start_date DESC LIMIT 1`; + const seasonRes = await this.pool.query(seasonQuery); + if (seasonRes.rows.length === 0) { + throw new Error('No active season found'); + } + const seasonId = seasonRes.rows[0].id; + + const query = ` + INSERT INTO gran_prix ( + date, track_id, season_id, has_sprint, has_x2, + race_results_id, qualifying_results_id, free_practice_results_id, sprint_results_id, full_race_results_id + ) + VALUES ( + $1, $2, $3, $4, $5, + ${data.has_x2 ? "NULL" : "nextval('results_id_seq')"}, + nextval('results_id_seq'), + nextval('results_id_seq'), + ${data.has_sprint ? "nextval('results_id_seq')" : "NULL"}, + ${data.has_x2 ? "nextval('results_id_seq')" : "NULL" } + ) + RETURNING id, date, track_id, has_sprint, has_x2 + `; + const values = [ + data.date, + data.track_id, + seasonId, + data.has_sprint ? 1 : 0, + data.has_x2 ? 1 : 0 + ]; + + const result = await this.pool.query(query, values); + const row = result.rows[0]; + + const trackRes = await this.pool.query('SELECT name FROM tracks WHERE id = $1', [row.track_id]); + const trackName = trackRes.rows[0]?.name || 'Unknown'; + + return { + ...row, + track_name: trackName, + has_sprint: row.has_sprint === 1, + has_x2: row.has_x2 === 1 + }; + } + + async updateGp(id: number, data: UpdateGpData): Promise { + const fields: string[] = []; + const values: any[] = []; + let idx = 1; + + if (data.date !== undefined) { + fields.push(`date = $${idx++}`); + values.push(data.date); + } + + if (data.has_sprint !== undefined) { + fields.push(`has_sprint = $${idx++}`); + values.push(data.has_sprint ? 1 : 0); + + if (data.has_sprint) { + fields.push(`sprint_results_id = COALESCE(sprint_results_id, nextval('results_id_seq'))`); + } else { + fields.push(`sprint_results_id = NULL`); + } + } + + if (data.has_x2 !== undefined) { + fields.push(`has_x2 = $${idx++}`); + values.push(data.has_x2 ? 1 : 0); + + if (data.has_x2) { + fields.push(`full_race_results_id = COALESCE(full_race_results_id, nextval('results_id_seq'))`); + fields.push(`race_results_id = NULL`); + } else { + fields.push(`full_race_results_id = NULL`); + fields.push(`race_results_id = COALESCE(race_results_id, nextval('results_id_seq'))`); + } + } + + if (fields.length === 0) return; + + values.push(id); + const query = `UPDATE gran_prix SET ${fields.join(', ')} WHERE id = $${idx}`; + await this.pool.query(query, values); + } + + async deleteGp(id: number): Promise { + await this.pool.query('DELETE FROM gran_prix WHERE id = $1', [id]); + } + + async bulkUpdateGpDate(daysOffset: number): Promise { + const seasonRes = await this.pool.query( + `SELECT id FROM seasons ORDER BY start_date DESC LIMIT 1` + ); + if (seasonRes.rows.length === 0) { + return; + } + const seasonId = seasonRes.rows[0].id; + + const query = ` + UPDATE gran_prix + SET date = date + make_interval(days => $1) + WHERE date > NOW() AND season_id = $2 + `; + // daysOffset can be negative + await this.pool.query(query, [daysOffset, seasonId]); + } + + async getAllTracks(): Promise<{id: number, name: string}[]> { + const result = await this.pool.query('SELECT id, name FROM tracks ORDER BY name'); + return result.rows; + } +} diff --git a/server/src/services/mail.service.ts b/server/src/services/mail.service.ts new file mode 100644 index 000000000..3a389e623 --- /dev/null +++ b/server/src/services/mail.service.ts @@ -0,0 +1,141 @@ +import { Pool } from "pg"; +import { getUpcomingRaceEmailTemplate } from "../config/email_templates.js"; +import nodemailer from "nodemailer"; + +export class EmailService { + FROM = "noreply@raceforfederica.com"; + pool = new Pool({ + connectionString: process.env.RACEFORFEDERICA_DB_DATABASE_URL, + ssl: true, + }); + + /** + * Send email using Gmail and Nodemailer + * @param to recipient email + * @param subject email subject + * @param htmlContent HTML body + * @param textContent plain text body + */ + async sendGmailEmail(to: string, subject: string, htmlContent: string, textContent?: string) { + // Gmail credentials from environment variables + const gmailUser = process.env.GMAIL_USER; + const gmailPass = process.env.GMAIL_PASS; + if (!gmailUser || !gmailPass) throw new Error("Gmail credentials not set"); + + // Create Nodemailer transporter + const transporter = nodemailer.createTransport({ + host: "smtp.gmail.com", + port: 465, + secure: true, + auth: { + user: gmailUser, + pass: gmailPass, + }, + }); + + // Send mail + const mailOptions = { + from: this.FROM, + to, + subject, + html: htmlContent, + text: textContent, + replyTo: this.FROM, + }; + try { + await transporter.sendMail(mailOptions); + return "success"; + } catch (error) { + return `Failed: ${error}`; + } + } + + // Scheduled chron job to send emails about upcoming races + async sendIncomingRaceMail() { + try { + // Check if the job is enabled via property table + const propertyResult = await this.pool.query(` + SELECT value + FROM property + WHERE name = 'send_incoming_race_mail_enabled' + LIMIT 1; + `); + + // If property doesn't exist or value is '0', exit early + if (propertyResult.rows.length === 0 || propertyResult.rows[0].value === '0') { + console.log("sendIncomingRaceMail job is disabled via property table"); + return; + } + + // Get upcoming races within 4 hours + const upcomingRacesResult = await this.pool.query(` + SELECT gp.id, gp.date, t.name as track_name, t.country, gp.has_sprint, gp.has_x2 + FROM gran_prix gp + JOIN tracks t ON gp.track_id = t.id + WHERE gp.date >= NOW() + AND gp.date <= NOW() + INTERVAL '12 hours' + ORDER BY gp.date ASC + LIMIT 2; + `); + + // FOR TESTING PURPOSES ONLY - COMMENT THE ABOVE AND UNCOMMENT BELOW + // const upcomingRacesResult = await this.pool.query(` + // SELECT gp.id, gp.date, t.name as track_name, t.country, gp.has_sprint, gp.has_x2 + // FROM gran_prix gp + // JOIN tracks t ON gp.track_id = t.id + // ORDER BY gp.date ASC + // LIMIT 2; + // `); + + const upcomingRaces = upcomingRacesResult.rows; + + // If no races are starting soon, exit early + if (upcomingRaces.length === 0) { + console.log("No upcoming races"); + return; + } + + // Get active users' emails + const activeUsersResult = await this.pool.query(` + SELECT id, username, name, surname, mail + FROM users + WHERE is_active = true AND mail IS NOT NULL AND mail != ''; + `); + + const activeUsers = activeUsersResult.rows; + + if (activeUsers.length === 0) { + console.log("No active users found"); + return; + } + + // Send emails to all active users + const emailPromises = activeUsers.map(async (user) => { + try { + // Use the mail field from the database + const userEmail = user.mail; + + // Generate email template using the email templates module + const { html, text, subject } = getUpcomingRaceEmailTemplate( + { username: user.username, name: user.name, surname: user.surname }, + upcomingRaces + ); + + const result = await this.sendGmailEmail(userEmail, subject, html, text); + console.log(`Email sent to ${user.name} ${user.surname} (${userEmail}): ${result}`); + return result; + } catch (error) { + console.error(`Failed to send email to ${user.name} ${user.surname}:`, error); + return `Failed: ${error}`; + } + }); + + const results = await Promise.all(emailPromises); + console.log(`Sent ${results.filter(r => r === 'success').length} successful emails out of ${activeUsers.length} attempts`); + + } catch (error) { + console.error("Error in sendIncomingRaceMail:", error); + } + } + +} \ No newline at end of file diff --git a/server/src/services/playground.service.ts b/server/src/services/playground.service.ts new file mode 100644 index 000000000..8ad7c46e3 --- /dev/null +++ b/server/src/services/playground.service.ts @@ -0,0 +1,25 @@ +import pg from "pg"; +import type { PlaygroundBestScore } from '@f123dashboard/shared'; + +export class PlaygroundService { + constructor(private pool: pg.Pool) {} + + async getPlaygroundLeaderboard(): Promise { + const result = await this.pool.query(` + SELECT user_id, username, encode(image, 'escape') as image, best_score, best_date + FROM playground_leaderboard + `); + return result.rows as PlaygroundBestScore[]; + } + + async setUserBestScore(score: PlaygroundBestScore): Promise { + await this.pool.query(` + INSERT INTO playground (id, user_id, best_score, best_date) + VALUES ($1, $1, $2, $3) + ON CONFLICT (id) + DO UPDATE SET + best_score = EXCLUDED.best_score, + best_date = EXCLUDED.best_date; + `, [score.user_id, score.best_score, score.best_date]); + } +} diff --git a/server/src/services/twitch.service.ts b/server/src/services/twitch.service.ts new file mode 100644 index 000000000..d96a8fff3 --- /dev/null +++ b/server/src/services/twitch.service.ts @@ -0,0 +1,60 @@ +import axios from 'axios'; +import type { TwitchTokenResponse, TwitchStreamResponse } from '@f123dashboard/shared'; + +export class TwitchService { + private clientId: string = '76pgow7h813kegw9ivqb9c63anh2uc'; + private clientSecret: string = process.env.RACEFORFEDERICA_DREANDOS_SECRET ?? ""; + private tokenUrl = 'https://id.twitch.tv/oauth2/token'; + private apiUrl = 'https://api.twitch.tv/helix'; + private accessToken: string | null = null; + private tokenExpiration: number = 0; + + private async getAccessToken(): Promise { + if (this.accessToken && Date.now() < this.tokenExpiration) { + return this.accessToken; + } + + if (!this.clientSecret) { + throw new Error("Twitch client secret is not set."); + } + + const body = new URLSearchParams(); + body.set('client_id', this.clientId); + body.set('client_secret', this.clientSecret); + body.set('grant_type', 'client_credentials'); + + try { + const response = await axios.post( + this.tokenUrl, + body.toString(), + { + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + } + ); + + this.accessToken = response.data.access_token; + this.tokenExpiration = Date.now() + (response.data.expires_in * 1000); + return this.accessToken; + } catch (error) { + throw new Error(`Failed to fetch access token: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + + async getStreamInfo(channelName: string): Promise { + try { + const token = await this.getAccessToken(); + const response = await axios.get( + `${this.apiUrl}/streams?user_login=${channelName}`, + { + headers: { + 'Client-ID': this.clientId, + 'Authorization': `Bearer ${token}` + } + } + ); + return response.data; + } catch (error) { + throw new Error(`Failed to fetch stream info: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } +} diff --git a/server/src/tests/auth.service.test.ts b/server/src/tests/auth.service.test.ts new file mode 100644 index 000000000..8c9d5ece6 --- /dev/null +++ b/server/src/tests/auth.service.test.ts @@ -0,0 +1,227 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { AuthService } from '../services/auth.service.js'; +import { createHash } from 'crypto'; + +describe('AuthService', () => { + let authService: AuthService; + let mockPool: any; + const TEST_JWT_SECRET = 'test-secret'; + + beforeEach(() => { + vi.stubEnv('JWT_SECRET', TEST_JWT_SECRET); + + mockPool = { + query: vi.fn(), + }; + + authService = new AuthService(mockPool); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + describe('constructor', () => { + it('should throw error if JWT_SECRET is not set', () => { + vi.stubEnv('JWT_SECRET', ''); + expect(() => new AuthService(mockPool)).toThrow('JWT_SECRET environment variable is required'); + }); + }); + + describe('login', () => { + const validLoginData = { + username: 'testuser', + password: 'Password123!', + userAgent: 'Mozilla/5.0' + }; + + const hashedPassword = createHash('sha256').update(validLoginData.password).digest('hex'); + + const mockDbUser = { + id: 1, + username: 'testuser', + name: 'Test', + surname: 'User', + password: hashedPassword, + mail: 'test@example.com', + image: 'base64image', + is_active: true, + is_admin: false + }; + + it('should login successfully with valid credentials', async () => { + // Mock user fetch + mockPool.query.mockResolvedValueOnce({ rows: [mockDbUser], rowCount: 1 }); + // Mock session creation (user fetch inside createSession) + mockPool.query.mockResolvedValueOnce({ rows: [{ username: 'testuser', is_admin: false }] }); + // Mock session insert + mockPool.query.mockResolvedValueOnce({ rowCount: 1 }); + // Mock last login update + mockPool.query.mockResolvedValueOnce({ rowCount: 1 }); + + const result = await authService.login(validLoginData); + + expect(result.success).toBe(true); + expect(result.token).toBeDefined(); + expect(result.user).toBeDefined(); + expect(result.user?.username).toBe(validLoginData.username); + }); + + it('should fail if user does not exist', async () => { + mockPool.query.mockResolvedValueOnce({ rows: [], rowCount: 0 }); + + const result = await authService.login(validLoginData); + + expect(result.success).toBe(false); + expect(result.message).toBe('Nome utente o password non validi'); + }); + + it('should fail if user is inactive', async () => { + mockPool.query.mockResolvedValueOnce({ + rows: [{ ...mockDbUser, is_active: false }], + rowCount: 1 + }); + + const result = await authService.login(validLoginData); + + expect(result.success).toBe(false); + expect(result.message).toBe('Account disabilitato'); + }); + + it('should fail with incorrect password', async () => { + mockPool.query.mockResolvedValueOnce({ rows: [mockDbUser], rowCount: 1 }); + + const result = await authService.login({ ...validLoginData, password: 'WrongPassword123!' }); + + expect(result.success).toBe(false); + expect(result.message).toBe('Nome utente o password non validi'); + }); + }); + + describe('register', () => { + const validRegisterData = { + username: 'newuser', + password: 'Password123!', + name: 'New', + surname: 'User', + mail: 'new@example.com', + image: 'base64data' + }; + + it('should register successfully', async () => { + // Check existing user (username) + mockPool.query.mockResolvedValueOnce({ rows: [], rowCount: 0 }); + // Check existing user (email) + mockPool.query.mockResolvedValueOnce({ rows: [], rowCount: 0 }); + + const newUser = { + id: 2, + username: validRegisterData.username, + name: validRegisterData.name, + surname: validRegisterData.surname, + mail: validRegisterData.mail, + image: validRegisterData.image, + is_active: true, + is_admin: false + }; + + // Insert user + mockPool.query.mockResolvedValueOnce({ rows: [newUser] }); + + // createSession > select user info + mockPool.query.mockResolvedValueOnce({ rows: [{ username: 'newuser', is_admin: false }] }); + // createSession > insert session + mockPool.query.mockResolvedValueOnce({ rowCount: 1 }); + + const result = await authService.register(validRegisterData); + + expect(result.success).toBe(true); + expect(result.message).toBe('Registrazione completata con successo'); + expect(result.user).toBeDefined(); + expect(result.user?.username).toBe(validRegisterData.username); + }); + + it('should fail if username already exists', async () => { + // Username exists + mockPool.query.mockResolvedValueOnce({ rows: [{ id: 1 }], rowCount: 1 }); + + const result = await authService.register(validRegisterData); + + expect(result.success).toBe(false); + expect(result.message).toBe('Nome utente già esistente'); + }); + + it('should fail validation for weak password', async () => { + const result = await authService.register({ ...validRegisterData, password: 'weak' }); + + expect(result.success).toBe(false); + expect(result.message).toBe('La password deve contenere almeno 8 caratteri'); + }); + + it('should fail validation for invalid email', async () => { + const result = await authService.register({ ...validRegisterData, mail: 'invalid-email' }); + + expect(result.success).toBe(false); + expect(result.message).toBe('Inserire un indirizzo email valido'); + }); + }); + + describe('validateToken', () => { + it('should validate a valid token', async () => { + // Create a real token using the mock service's secret (from stubEnv) + const token = require('jsonwebtoken').sign( + { userId: 1, username: 'user', isAdmin: false }, + TEST_JWT_SECRET + ); + + const mockDbUser = { + id: 1, + username: 'user', + is_active: true, + name: 'User', + surname: 'Test', + mail: 'user@test.com', + image: 'img', + is_admin: false + }; + + // 1. Select user + mockPool.query.mockResolvedValueOnce({ rows: [mockDbUser], rowCount: 1 }); + + // 2. Select active session + mockPool.query.mockResolvedValueOnce({ + rows: [{ + session_token: 'abc', + expires_at: new Date(Date.now() + 100000).toISOString(), + is_active: true + }], + rowCount: 1 + }); + + // 3. Update last activity + mockPool.query.mockResolvedValueOnce({ rowCount: 1 }); + + const result = await authService.validateToken(token); + + expect(result.valid).toBe(true); + // validateToken returns flat structure, not nested user object + expect((result as any).username).toBe('user'); + }); + + it('should reject invalid token', async () => { + const result = await authService.validateToken('invalid.token.here'); + expect(result.valid).toBe(false); + }); + }); + + describe('cleanupExpiredSessions', () => { + it('should return success after queries', async () => { + mockPool.query.mockResolvedValue({}); + + const result = await authService.cleanupExpiredSessions(); + + expect(result.success).toBe(true); + expect(mockPool.query).toHaveBeenCalledTimes(2); // Update active=false, Delete older than 30 days + }); + }); +}); diff --git a/server/src/tests/gp-edit.test.ts b/server/src/tests/gp-edit.test.ts new file mode 100644 index 000000000..66953f004 --- /dev/null +++ b/server/src/tests/gp-edit.test.ts @@ -0,0 +1,109 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../server.js'; // Ensure server.ts exports 'app' default (it does) +import pool from '../config/db.js'; + +// Mock auth middleware to bypass checks +vi.mock('../middleware/auth.middleware.js', async () => { + const mocks = await import('./mocks'); + return mocks.mockAuthMiddleware; +}); + +// Mock DB pool +vi.mock('../config/db.js', async () => { + const mocks = await import('./mocks'); + return mocks.mockDb; +}); + +describe('GP Edit API', () => { + beforeEach(() => { + vi.resetAllMocks(); + // Default fallback to prevent "undefined" errors if mocks are exhausted + (pool.query as any).mockResolvedValue({ rows: [], rowCount: 0 }); + }); + + describe('POST /api/gp-edit/list', () => { + it('should return list of upcoming GPs', async () => { + // Mock season fetch + (pool.query as any).mockResolvedValueOnce({ rows: [{ id: 2025 }] }); + + const mockRows = [ + { id: 1, date: new Date(), track_id: 1, track_name: 'Monza', has_sprint: 0, has_x2: 0 } + ]; + // Mock list fetch + (pool.query as any).mockResolvedValueOnce({ rows: mockRows }); + + const res = await request(app).post('/api/gp-edit/list'); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.data).toHaveLength(1); + expect(res.body.data[0].track_name).toBe('Monza'); + expect(res.body.data[0].has_sprint).toBe(false); // Converted in service + }); + + it('should handle errors', async () => { + // Mock season fetch failure + (pool.query as any).mockRejectedValueOnce(new Error('DB Error')); + + const res = await request(app).post('/api/gp-edit/list'); + + expect(res.status).toBe(500); + expect(res.body.success).toBe(false); + }); + }); + + describe('POST /api/gp-edit/create', () => { + it('should create a GP', async () => { + // Mock season fetch + (pool.query as any).mockResolvedValueOnce({ rows: [{ id: 10 }] }); + // Mock insert + (pool.query as any).mockResolvedValueOnce({ rows: [{ id: 100, date: '2025-01-01', track_id: 5, has_sprint: 1, has_x2: 0 }] }); + // Mock track fetch + (pool.query as any).mockResolvedValueOnce({ rows: [{ name: 'Bahrain' }] }); + + const res = await request(app) + .post('/api/gp-edit/create') + .send({ date: '2025-01-01', track_id: 5, has_sprint: true, has_x2: false }); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.data.track_name).toBe('Bahrain'); + }); + }); + + describe('POST /api/gp-edit/update', () => { + it('should update GP and handle sprint ID logic', async () => { + (pool.query as any).mockResolvedValueOnce({ rowCount: 1 }); + + // Corrected route path: /api/gp-edit/update/:id + const res = await request(app) + .post('/api/gp-edit/update/1') + .send({ has_sprint: true }); + + expect(res.status).toBe(200); + // Verify query contains logic from refactoring + // But we call clearAllMocks. check calls[0]. + const callArgs = (pool.query as any).mock.calls[0]; + const querySql = callArgs[0]; + expect(querySql).toContain('sprint_results_id = COALESCE'); + }); + }); + + describe('POST /api/gp-edit/bulk-update-date', () => { + it('should update dates', async () => { + // Mock season fetch + (pool.query as any).mockResolvedValueOnce({ rows: [{ id: 2025 }] }); + // Mock update + (pool.query as any).mockResolvedValueOnce({ rowCount: 5 }); + + const res = await request(app) + .post('/api/gp-edit/bulk-update-date') + .send({ daysOffset: 7 }); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + expect(pool.query).toHaveBeenLastCalledWith(expect.stringContaining('UPDATE gran_prix'), [7, 2025]); + }); + }); +}); diff --git a/server/src/tests/mocks.ts b/server/src/tests/mocks.ts new file mode 100644 index 000000000..2bb479359 --- /dev/null +++ b/server/src/tests/mocks.ts @@ -0,0 +1,17 @@ +import { vi } from 'vitest'; + +export const mockAuthMiddleware = { + authMiddleware: (req: any, res: any, next: any) => { + req.user = { isAdmin: true, userId: 1, username: 'admin' }; + next(); + }, + adminMiddleware: (req: any, res: any, next: any) => { + next(); + } +}; + +export const mockDb = { + default: { + query: vi.fn(), + } +}; diff --git a/server/src/tests/setup.test.ts b/server/src/tests/setup.test.ts new file mode 100644 index 000000000..847bf0f47 --- /dev/null +++ b/server/src/tests/setup.test.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest'; + +describe('Setup', () => { + it('should run tests', () => { + expect(true).toBe(true); + }); +}); diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 000000000..ac83ffd87 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "node", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/server/vitest.config.ts b/server/vitest.config.ts new file mode 100644 index 000000000..4b2dff04d --- /dev/null +++ b/server/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + globals: true, + fileParallelism: false, + env: { + JWT_SECRET: 'test-secret', + }, + }, +}); diff --git a/shared/package.json b/shared/package.json new file mode 100644 index 000000000..4a0553194 --- /dev/null +++ b/shared/package.json @@ -0,0 +1,24 @@ +{ + "name": "@f123dashboard/shared", + "version": "1.0.0", + "description": "Shared TypeScript types for F123 Dashboard", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "scripts": { + "build": "tsc", + "watch": "tsc --watch", + "clean": "rimraf dist" + }, + "keywords": [ + "types", + "shared", + "f123" + ], + "author": "", + "license": "ISC", + "devDependencies": { + "typescript": "^5.7.2", + "rimraf": "^6.0.1" + } +} diff --git a/shared/src/index.ts b/shared/src/index.ts new file mode 100644 index 000000000..30402e368 --- /dev/null +++ b/shared/src/index.ts @@ -0,0 +1,8 @@ +// Barrel export for all shared types +export * from './models/auth.js'; +export * from './models/database.js'; +export * from './models/fanta.js'; +export * from './models/playground.js'; +export * from './models/twitch.js'; +export * from './models/gp-edit.js'; + diff --git a/shared/src/models/auth.ts b/shared/src/models/auth.ts new file mode 100644 index 000000000..93336bf57 --- /dev/null +++ b/shared/src/models/auth.ts @@ -0,0 +1,88 @@ +// Auth types + +// Base response type for all API responses +export type BaseResponse = { + success: boolean; + message?: string; +} + +export type User = { + id: number; + username: string; + name: string; + surname: string; + mail?: string; + image?: string; + isAdmin?: boolean; +} + +export type LoginRequest = { + username: string; + password: string; + userAgent?: string; +} + +export type AuthResponse = BaseResponse & { + user?: User; + token?: string; +} + +export type ChangePasswordRequest = { + currentPassword: string; + newPassword: string; + jwtToken: string; +} + +export type AdminChangePasswordRequest = { + userName: string; + newPassword: string; + jwtToken: string; +} + +export type ChangePasswordResponse = BaseResponse; + +export type UserSession = { + sessionToken: string; + createdAt: string; + lastActivity: string; + expiresAt: string; + isActive: boolean; + isCurrent: boolean; +} + +export type SessionsResponse = BaseResponse & { + sessions?: UserSession[]; +} + +export type TokenValidationResponse = { + valid: boolean; + userId?: number; + username?: string; + name?: string; + surname?: string; + mail?: string; + image?: string; + isAdmin?: boolean; +} + +export type RefreshTokenResponse = BaseResponse & { + token?: string; +} + +export type LogoutResponse = BaseResponse; + +export type RegisterRequest = { + username: string; + name: string; + surname: string; + password: string; + mail: string; + image: string; +} + +export type UpdateUserInfoRequest = { + name?: string; + surname?: string; + mail?: string; + image?: string; +} diff --git a/shared/src/models/database.ts b/shared/src/models/database.ts new file mode 100644 index 000000000..5be4eaf03 --- /dev/null +++ b/shared/src/models/database.ts @@ -0,0 +1,134 @@ +// Database types +export type DriverData = { + driver_id: number; + driver_username: string; + driver_name: string; + driver_surname: string; + driver_description: string; + driver_license_pt: number; + driver_consistency_pt: number; + driver_fast_lap_pt: number; + drivers_dangerous_pt: number; + driver_ingenuity_pt: number; + driver_strategy_pt: number; + driver_color: string; + car_name: string; + car_overall_score: number; + total_sprint_points: number; + total_free_practice_points: number; + total_qualifying_points: number; + total_full_race_points: number; + total_race_points: number; + total_points: number; +}; + +export type Driver = { + id: number; + username: string; + first_name: string; + surname: string; +} + +export type SessionResult = { + position: number; + driver_username: string; + fast_lap: boolean | null; +} + +export type ChampionshipData = { + gran_prix_id: number; + track_name: string; + gran_prix_date: Date; + gran_prix_has_sprint: number; + gran_prix_has_x2: number; + track_country: string; + sessions: { + free_practice?: SessionResult[]; + qualifying?: SessionResult[]; + race?: SessionResult[]; + sprint?: SessionResult[]; + full_race?: SessionResult[]; + }; + fastLapDrivers: { + race?: string; + sprint?: string; + full_race?: string; + }; +} + +export type Season = { + id: number; + description: string; + startDate?: Date; + endDate?: Date; +} + +export type CumulativePointsData = { + date: string; + track_name: string; + driver_id: number; + driver_username: string; + driver_color: string; + cumulative_points: number; +} + +export type TrackData = { + track_id: number; + name: string; + date: string; + has_sprint: number; + has_x2: number; + country: string; + besttime_driver_time: string; + username: string; +} + +export type RaceResult = { + id: number; + track_id: number; + id_1_place: number; + id_2_place: number; + id_3_place: number; + id_4_place: number; + id_5_place: number; + id_6_place: number; + id_7_place: number; + id_8_place: number; + id_fast_lap: number; + list_dnf: string; +} + +export type Constructor = { + constructor_id: number; + constructor_name: string; + constructor_color: string; + driver_1_id: number; + driver_1_username: string; + driver_1_tot_points: number; + driver_2_id: number; + driver_2_username: string; + driver_2_tot_points: number; + constructor_tot_points: number; + constructor_race_points?: number; + constructor_full_race_points?: number; + constructor_sprint_points?: number; + constructor_qualifying_points?: number; + constructor_free_practice_points?: number; + constructor_gained_points?: number; +} + +export type ConstructorGrandPrixPoints = { + constructor_id: number; + constructor_name: string; + grand_prix_id: number; + grand_prix_date: string; + track_name: string; + track_id: number; + season_id: number; + season_description: string; + driver_id_1: number; + driver_id_2: number; + driver_1_points: number; + driver_2_points: number; + constructor_points: number; +} diff --git a/shared/src/models/fanta.ts b/shared/src/models/fanta.ts new file mode 100644 index 000000000..70eb8f20b --- /dev/null +++ b/shared/src/models/fanta.ts @@ -0,0 +1,18 @@ +// Fanta types +export type FantaVote = { + fanta_player_id: number; + username: string; + track_id: number; + id_1_place: number; + id_2_place: number; + id_3_place: number; + id_4_place: number; + id_5_place: number; + id_6_place: number; + id_7_place: number; + id_8_place: number; + id_fast_lap: number; + id_dnf: number; + season_id?: number; + constructor_id: number; +}; diff --git a/shared/src/models/gp-edit.ts b/shared/src/models/gp-edit.ts new file mode 100644 index 000000000..1bba30f70 --- /dev/null +++ b/shared/src/models/gp-edit.ts @@ -0,0 +1,21 @@ +export interface GPEditItem { + id: number; + date: Date; + track_id: number; + track_name: string; + has_sprint: boolean; + has_x2: boolean; + } + + export interface CreateGpData { + track_id: number; + date: string; + has_sprint: boolean; + has_x2: boolean; + } + + export interface UpdateGpData { + date?: string; + has_sprint?: boolean; + has_x2?: boolean; + } diff --git a/shared/src/models/playground.ts b/shared/src/models/playground.ts new file mode 100644 index 000000000..8f3a71d6a --- /dev/null +++ b/shared/src/models/playground.ts @@ -0,0 +1,8 @@ +// Playground types +export type PlaygroundBestScore = { + user_id: number; + username: string; + image: string; + best_score: number; + best_date: Date; +}; diff --git a/shared/src/models/twitch.ts b/shared/src/models/twitch.ts new file mode 100644 index 000000000..3740958f9 --- /dev/null +++ b/shared/src/models/twitch.ts @@ -0,0 +1,19 @@ +// Twitch types +export type TwitchTokenResponse = { + access_token: string; + expires_in: number; +} + +export type TwitchStreamResponse = { + data: Array<{ + id: string; + user_id: string; + user_login: string; + type: string; + title: string; + viewer_count: number; + started_at: string; + language: string; + thumbnail_url: string; + }>; +} diff --git a/shared/tsconfig.json b/shared/tsconfig.json new file mode 100644 index 000000000..8a0c735e2 --- /dev/null +++ b/shared/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022"], + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/src/app/app.config.ts b/src/app/app.config.ts deleted file mode 100644 index 0b96c6d8f..000000000 --- a/src/app/app.config.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ApplicationConfig, importProvidersFrom } from '@angular/core'; -import { provideAnimations } from '@angular/platform-browser/animations'; -import { - provideRouter, - withEnabledBlockingInitialNavigation, - withHashLocation, - withInMemoryScrolling, - withRouterConfig, - withViewTransitions -} from '@angular/router'; - -import { DropdownModule, SidebarModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { routes } from './app.routes'; - -export const appConfig: ApplicationConfig = { - providers: [ - provideRouter(routes, - withRouterConfig({ - onSameUrlNavigation: 'reload' - }), - withInMemoryScrolling({ - scrollPositionRestoration: 'top', - anchorScrolling: 'enabled' - }), - withEnabledBlockingInitialNavigation(), - withViewTransitions(), - withHashLocation() - ), - importProvidersFrom(SidebarModule, DropdownModule), - IconSetService, - provideAnimations() - ] -}; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts deleted file mode 100644 index 4727d0198..000000000 --- a/src/app/app.routes.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { Routes } from '@angular/router'; -import { DefaultLayoutComponent } from './layout'; - -export const routes: Routes = [ - { - path: '', - redirectTo: 'dashboard', - pathMatch: 'full' - }, - { - path: '', - component: DefaultLayoutComponent, - data: { - title: 'Home' - }, - children: [ - { - path: 'dashboard', - loadChildren: () => import('./views/dashboard/routes').then((m) => m.routes) - }, - { - path: 'theme', - loadChildren: () => import('./views/theme/routes').then((m) => m.routes) - }, - { - path: 'base', - loadChildren: () => import('./views/base/routes').then((m) => m.routes) - }, - { - path: 'buttons', - loadChildren: () => import('./views/buttons/routes').then((m) => m.routes) - }, - { - path: 'forms', - loadChildren: () => import('./views/forms/routes').then((m) => m.routes) - }, - { - path: 'icons', - loadChildren: () => import('./views/icons/routes').then((m) => m.routes) - }, - { - path: 'notifications', - loadChildren: () => import('./views/notifications/routes').then((m) => m.routes) - }, - { - path: 'widgets', - loadChildren: () => import('./views/widgets/routes').then((m) => m.routes) - }, - { - path: 'charts', - loadChildren: () => import('./views/charts/routes').then((m) => m.routes) - }, - { - path: 'pages', - loadChildren: () => import('./views/pages/routes').then((m) => m.routes) - } - ] - }, - { - path: '404', - loadComponent: () => import('./views/pages/page404/page404.component').then(m => m.Page404Component), - data: { - title: 'Page 404' - } - }, - { - path: '500', - loadComponent: () => import('./views/pages/page500/page500.component').then(m => m.Page500Component), - data: { - title: 'Page 500' - } - }, - { - path: 'login', - loadComponent: () => import('./views/pages/login/login.component').then(m => m.LoginComponent), - data: { - title: 'Login Page' - } - }, - { - path: 'register', - loadComponent: () => import('./views/pages/register/register.component').then(m => m.RegisterComponent), - data: { - title: 'Register Page' - } - }, - { path: '**', redirectTo: 'dashboard' } -]; diff --git a/src/app/layout/default-layout/_nav.ts b/src/app/layout/default-layout/_nav.ts deleted file mode 100644 index 2810bda12..000000000 --- a/src/app/layout/default-layout/_nav.ts +++ /dev/null @@ -1,292 +0,0 @@ -import { INavData } from '@coreui/angular'; - -export const navItems: INavData[] = [ - { - name: 'Dashboard', - url: '/dashboard', - iconComponent: { name: 'cil-speedometer' }, - badge: { - color: 'info', - text: 'NEW' - } - }, - { - title: true, - name: 'Theme' - }, - { - name: 'Colors', - url: '/theme/colors', - iconComponent: { name: 'cil-drop' } - }, - { - name: 'Typography', - url: '/theme/typography', - linkProps: { fragment: 'headings' }, - iconComponent: { name: 'cil-pencil' } - }, - { - name: 'Components', - title: true - }, - { - name: 'Base', - url: '/base', - iconComponent: { name: 'cil-puzzle' }, - children: [ - { - name: 'Accordion', - url: '/base/accordion', - icon: 'nav-icon-bullet' - }, - { - name: 'Breadcrumbs', - url: '/base/breadcrumbs', - icon: 'nav-icon-bullet' - }, - { - name: 'Cards', - url: '/base/cards', - icon: 'nav-icon-bullet' - }, - { - name: 'Carousel', - url: '/base/carousel', - icon: 'nav-icon-bullet' - }, - { - name: 'Collapse', - url: '/base/collapse', - icon: 'nav-icon-bullet' - }, - { - name: 'List Group', - url: '/base/list-group', - icon: 'nav-icon-bullet' - }, - { - name: 'Navs & Tabs', - url: '/base/navs', - icon: 'nav-icon-bullet' - }, - { - name: 'Pagination', - url: '/base/pagination', - icon: 'nav-icon-bullet' - }, - { - name: 'Placeholder', - url: '/base/placeholder', - icon: 'nav-icon-bullet' - }, - { - name: 'Popovers', - url: '/base/popovers', - icon: 'nav-icon-bullet' - }, - { - name: 'Progress', - url: '/base/progress', - icon: 'nav-icon-bullet' - }, - { - name: 'Spinners', - url: '/base/spinners', - icon: 'nav-icon-bullet' - }, - { - name: 'Tables', - url: '/base/tables', - icon: 'nav-icon-bullet' - }, - { - name: 'Tabs', - url: '/base/tabs', - icon: 'nav-icon-bullet' - }, - { - name: 'Tooltips', - url: '/base/tooltips', - icon: 'nav-icon-bullet' - } - ] - }, - { - name: 'Buttons', - url: '/buttons', - iconComponent: { name: 'cil-cursor' }, - children: [ - { - name: 'Buttons', - url: '/buttons/buttons', - icon: 'nav-icon-bullet' - }, - { - name: 'Button groups', - url: '/buttons/button-groups', - icon: 'nav-icon-bullet' - }, - { - name: 'Dropdowns', - url: '/buttons/dropdowns', - icon: 'nav-icon-bullet' - } - ] - }, - { - name: 'Forms', - url: '/forms', - iconComponent: { name: 'cil-notes' }, - children: [ - { - name: 'Form Control', - url: '/forms/form-control', - icon: 'nav-icon-bullet' - }, - { - name: 'Select', - url: '/forms/select', - icon: 'nav-icon-bullet' - }, - { - name: 'Checks & Radios', - url: '/forms/checks-radios', - icon: 'nav-icon-bullet' - }, - { - name: 'Range', - url: '/forms/range', - icon: 'nav-icon-bullet' - }, - { - name: 'Input Group', - url: '/forms/input-group', - icon: 'nav-icon-bullet' - }, - { - name: 'Floating Labels', - url: '/forms/floating-labels', - icon: 'nav-icon-bullet' - }, - { - name: 'Layout', - url: '/forms/layout', - icon: 'nav-icon-bullet' - }, - { - name: 'Validation', - url: '/forms/validation', - icon: 'nav-icon-bullet' - } - ] - }, - { - name: 'Charts', - iconComponent: { name: 'cil-chart-pie' }, - url: '/charts' - }, - { - name: 'Icons', - iconComponent: { name: 'cil-star' }, - url: '/icons', - children: [ - { - name: 'CoreUI Free', - url: '/icons/coreui-icons', - icon: 'nav-icon-bullet', - badge: { - color: 'success', - text: 'FREE' - } - }, - { - name: 'CoreUI Flags', - url: '/icons/flags', - icon: 'nav-icon-bullet' - }, - { - name: 'CoreUI Brands', - url: '/icons/brands', - icon: 'nav-icon-bullet' - } - ] - }, - { - name: 'Notifications', - url: '/notifications', - iconComponent: { name: 'cil-bell' }, - children: [ - { - name: 'Alerts', - url: '/notifications/alerts', - icon: 'nav-icon-bullet' - }, - { - name: 'Badges', - url: '/notifications/badges', - icon: 'nav-icon-bullet' - }, - { - name: 'Modal', - url: '/notifications/modal', - icon: 'nav-icon-bullet' - }, - { - name: 'Toast', - url: '/notifications/toasts', - icon: 'nav-icon-bullet' - } - ] - }, - { - name: 'Widgets', - url: '/widgets', - iconComponent: { name: 'cil-calculator' }, - badge: { - color: 'info', - text: 'NEW' - } - }, - { - title: true, - name: 'Extras' - }, - { - name: 'Pages', - url: '/login', - iconComponent: { name: 'cil-star' }, - children: [ - { - name: 'Login', - url: '/login', - icon: 'nav-icon-bullet' - }, - { - name: 'Register', - url: '/register', - icon: 'nav-icon-bullet' - }, - { - name: 'Error 404', - url: '/404', - icon: 'nav-icon-bullet' - }, - { - name: 'Error 500', - url: '/500', - icon: 'nav-icon-bullet' - } - ] - }, - { - title: true, - name: 'Links', - class: 'mt-auto' - }, - { - name: 'Docs', - url: 'https://coreui.io/angular/docs/5.x/', - iconComponent: { name: 'cil-description' }, - attributes: { target: '_blank' } - } -]; diff --git a/src/app/layout/default-layout/default-footer/default-footer.component.html b/src/app/layout/default-layout/default-footer/default-footer.component.html deleted file mode 100644 index 4a4f3f617..000000000 --- a/src/app/layout/default-layout/default-footer/default-footer.component.html +++ /dev/null @@ -1,12 +0,0 @@ - -
- CoreUI - © 2024 creativeLabs -
-
- Powered by - - CoreUI for Angular - -
- diff --git a/src/app/layout/default-layout/default-header/default-header.component.html b/src/app/layout/default-layout/default-header/default-header.component.html deleted file mode 100644 index 33204f7be..000000000 --- a/src/app/layout/default-layout/default-header/default-header.component.html +++ /dev/null @@ -1,181 +0,0 @@ - - - - - - - Dashboard - - - Users - - - Settings - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- @for (mode of colorModes; track mode.name) { - - } -
-
-
diff --git a/src/app/layout/default-layout/default-header/default-header.component.ts b/src/app/layout/default-layout/default-header/default-header.component.ts deleted file mode 100644 index fb2083b4b..000000000 --- a/src/app/layout/default-layout/default-header/default-header.component.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { NgStyle, NgTemplateOutlet } from '@angular/common'; -import { Component, computed, inject, input } from '@angular/core'; -import { RouterLink, RouterLinkActive } from '@angular/router'; - -import { - AvatarComponent, - BadgeComponent, - BreadcrumbRouterComponent, - ColorModeService, - ContainerComponent, - DropdownComponent, - DropdownDividerDirective, - DropdownHeaderDirective, - DropdownItemDirective, - DropdownMenuDirective, - DropdownToggleDirective, - HeaderComponent, - HeaderNavComponent, - HeaderTogglerDirective, - NavItemComponent, - NavLinkDirective, - ProgressBarDirective, - ProgressComponent, - SidebarToggleDirective, - TextColorDirective, - ThemeDirective -} from '@coreui/angular'; - -import { IconDirective } from '@coreui/icons-angular'; - -@Component({ - selector: 'app-default-header', - templateUrl: './default-header.component.html', - standalone: true, - imports: [ContainerComponent, HeaderTogglerDirective, SidebarToggleDirective, IconDirective, HeaderNavComponent, NavItemComponent, NavLinkDirective, RouterLink, RouterLinkActive, NgTemplateOutlet, BreadcrumbRouterComponent, ThemeDirective, DropdownComponent, DropdownToggleDirective, TextColorDirective, AvatarComponent, DropdownMenuDirective, DropdownHeaderDirective, DropdownItemDirective, BadgeComponent, DropdownDividerDirective, ProgressBarDirective, ProgressComponent, NgStyle] -}) -export class DefaultHeaderComponent extends HeaderComponent { - - readonly #colorModeService = inject(ColorModeService); - readonly colorMode = this.#colorModeService.colorMode; - - readonly colorModes = [ - { name: 'light', text: 'Light', icon: 'cilSun' }, - { name: 'dark', text: 'Dark', icon: 'cilMoon' }, - { name: 'auto', text: 'Auto', icon: 'cilContrast' } - ]; - - readonly icons = computed(() => { - const currentMode = this.colorMode(); - return this.colorModes.find(mode => mode.name === currentMode)?.icon ?? 'cilSun'; - }); - - constructor() { - super(); - } - - sidebarId = input('sidebar1'); - - public newMessages = [ - { - id: 0, - from: 'Jessica Williams', - avatar: '7.jpg', - status: 'success', - title: 'Urgent: System Maintenance Tonight', - time: 'Just now', - link: 'apps/email/inbox/message', - message: 'Attention team, we\'ll be conducting critical system maintenance tonight from 10 PM to 2 AM. Plan accordingly...' - }, - { - id: 1, - from: 'Richard Johnson', - avatar: '6.jpg', - status: 'warning', - title: 'Project Update: Milestone Achieved', - time: '5 minutes ago', - link: 'apps/email/inbox/message', - message: 'Kudos on hitting sales targets last quarter! Let\'s keep the momentum. New goals, new victories ahead...' - }, - { - id: 2, - from: 'Angela Rodriguez', - avatar: '5.jpg', - status: 'danger', - title: 'Social Media Campaign Launch', - time: '1:52 PM', - link: 'apps/email/inbox/message', - message: 'Exciting news! Our new social media campaign goes live tomorrow. Brace yourselves for engagement...' - }, - { - id: 3, - from: 'Jane Lewis', - avatar: '4.jpg', - status: 'info', - title: 'Inventory Checkpoint', - time: '4:03 AM', - link: 'apps/email/inbox/message', - message: 'Team, it\'s time for our monthly inventory check. Accurate counts ensure smooth operations. Let\'s nail it...' - }, - { - id: 3, - from: 'Ryan Miller', - avatar: '4.jpg', - status: 'info', - title: 'Customer Feedback Results', - time: '3 days ago', - link: 'apps/email/inbox/message', - message: 'Our latest customer feedback is in. Let\'s analyze and discuss improvements for an even better service...' - } - ]; - - public newNotifications = [ - { id: 0, title: 'New user registered', icon: 'cilUserFollow', color: 'success' }, - { id: 1, title: 'User deleted', icon: 'cilUserUnfollow', color: 'danger' }, - { id: 2, title: 'Sales report is ready', icon: 'cilChartPie', color: 'info' }, - { id: 3, title: 'New client', icon: 'cilBasket', color: 'primary' }, - { id: 4, title: 'Server overloaded', icon: 'cilSpeedometer', color: 'warning' } - ]; - - public newStatus = [ - { id: 0, title: 'CPU Usage', value: 25, color: 'info', details: '348 Processes. 1/4 Cores.' }, - { id: 1, title: 'Memory Usage', value: 70, color: 'warning', details: '11444GB/16384MB' }, - { id: 2, title: 'SSD 1 Usage', value: 90, color: 'danger', details: '243GB/256GB' } - ]; - - public newTasks = [ - { id: 0, title: 'Upgrade NPM', value: 0, color: 'info' }, - { id: 1, title: 'ReactJS Version', value: 25, color: 'danger' }, - { id: 2, title: 'VueJS Version', value: 50, color: 'warning' }, - { id: 3, title: 'Add new layouts', value: 75, color: 'info' }, - { id: 4, title: 'Angular Version', value: 100, color: 'success' } - ]; - -} diff --git a/src/app/layout/default-layout/default-layout.component.html b/src/app/layout/default-layout/default-layout.component.html deleted file mode 100644 index fc1118286..000000000 --- a/src/app/layout/default-layout/default-layout.component.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - @if (!sidebar1.narrow) { - - - - } - - - -
- - - -
- - - -
- - -
diff --git a/src/app/layout/default-layout/default-layout.component.ts b/src/app/layout/default-layout/default-layout.component.ts deleted file mode 100644 index 03a3699b9..000000000 --- a/src/app/layout/default-layout/default-layout.component.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Component } from '@angular/core'; -import { RouterLink, RouterOutlet } from '@angular/router'; -import { NgScrollbar } from 'ngx-scrollbar'; - -import { IconDirective } from '@coreui/icons-angular'; -import { - ContainerComponent, - ShadowOnScrollDirective, - SidebarBrandComponent, - SidebarComponent, - SidebarFooterComponent, - SidebarHeaderComponent, - SidebarNavComponent, - SidebarToggleDirective, - SidebarTogglerDirective -} from '@coreui/angular'; - -import { DefaultFooterComponent, DefaultHeaderComponent } from './'; -import { navItems } from './_nav'; - -function isOverflown(element: HTMLElement) { - return ( - element.scrollHeight > element.clientHeight || - element.scrollWidth > element.clientWidth - ); -} - -@Component({ - selector: 'app-dashboard', - templateUrl: './default-layout.component.html', - styleUrls: ['./default-layout.component.scss'], - standalone: true, - imports: [ - SidebarComponent, - SidebarHeaderComponent, - SidebarBrandComponent, - RouterLink, - IconDirective, - NgScrollbar, - SidebarNavComponent, - SidebarFooterComponent, - SidebarToggleDirective, - SidebarTogglerDirective, - DefaultHeaderComponent, - ShadowOnScrollDirective, - ContainerComponent, - RouterOutlet, - DefaultFooterComponent - ] -}) -export class DefaultLayoutComponent { - public navItems = navItems; - - onScrollbarUpdate($event: any) { - // if ($event.verticalUsed) { - // console.log('verticalUsed', $event.verticalUsed); - // } - } -} diff --git a/src/app/views/base/accordion/accordions.component.html b/src/app/views/base/accordion/accordions.component.html deleted file mode 100644 index e27353d86..000000000 --- a/src/app/views/base/accordion/accordions.component.html +++ /dev/null @@ -1,127 +0,0 @@ - - - - - Angular Accordion - - -

- Click the accordions below to expand/collapse the accordion content. -

- - - - - - - -
-
-
- - - - - -
- This is the - #second - item accordion body. It is hidden by - default, until the collapse plugin adds the appropriate classes that we use to - style each element. These classes control the overall appearance, as well as - the showing and hiding via CSS transitions. You can modify any of this with - custom CSS or overriding our default variables. It's also worth noting - that just about any HTML can go within the .accordion-body, - though the transition does limit overflow. -
-
-
- - - - - -
- -
-
-
-
-
-
-
- - - Angular Accordion flush - - -

- Add flush to remove the default background-color, some - borders, and some rounded corners to render accordions edge-to-edge with their parent - container. -

- - - - - Accordion item #0 - - - - - - - - Accordion item #1 - - - - - - - - Accordion item #2 - - - - - - - -
-
- - - Angular Accordion alwaysOpen - - -

- Add alwaysOpen property to make accordion items stay open when another - item is opened. -

- - - @for(item of items; track item; let i = $index;) { - - - Custom Accordion item #{{ i }} - - - {{ i }}. - - - - } - - - -
-
-
-
diff --git a/src/app/views/base/accordion/accordions.component.scss b/src/app/views/base/accordion/accordions.component.scss deleted file mode 100644 index 0991fbe16..000000000 --- a/src/app/views/base/accordion/accordions.component.scss +++ /dev/null @@ -1,17 +0,0 @@ -:host ::ng-deep { - .accordion-custom { - .accordion-button { - background-color: var(--cui-dark); - color: var(--cui-white); - } - - .accordion-button::after { - --cui-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='rgba%28255, 255, 255, 0.87%29'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); - --cui-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='rgba%28255, 255, 255, 0.87%29'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); - } - } -} - - - - diff --git a/src/app/views/base/accordion/accordions.component.spec.ts b/src/app/views/base/accordion/accordions.component.spec.ts deleted file mode 100644 index 808df4c42..000000000 --- a/src/app/views/base/accordion/accordions.component.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { AccordionModule, CardModule, GridModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { AccordionsComponent } from './accordions.component'; - -describe('AccordionsComponent', () => { - let component: AccordionsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [AccordionModule, NoopAnimationsModule, CardModule, GridModule, RouterTestingModule, AccordionsComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(AccordionsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/base/accordion/accordions.component.ts b/src/app/views/base/accordion/accordions.component.ts deleted file mode 100644 index 3a5ddcf73..000000000 --- a/src/app/views/base/accordion/accordions.component.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Component } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, AccordionComponent, AccordionItemComponent, TemplateIdDirective, AccordionButtonDirective, BgColorDirective } from '@coreui/angular'; -import { DocsExampleComponent } from '@docs-components/public-api'; - -@Component({ - selector: 'app-accordions', - templateUrl: './accordions.component.html', - styleUrls: ['./accordions.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, AccordionComponent, AccordionItemComponent, TemplateIdDirective, AccordionButtonDirective, BgColorDirective] -}) -export class AccordionsComponent { - - items = [1, 2, 3, 4]; - - constructor( - private sanitizer: DomSanitizer - ) { } - - getAccordionBodyText(value: string|number) { - const textSample = ` - This is the #${value} item accordion body. It is hidden by - default, until the collapse plugin adds the appropriate classes that we use to - style each element. These classes control the overall appearance, as well as - the showing and hiding via CSS transitions. You can modify any of this with - custom CSS or overriding our default variables. It's also worth noting - that just about any HTML can go within the .accordion-body, - though the transition does limit overflow. - `; - return this.sanitizer.bypassSecurityTrustHtml(textSample); - } -} diff --git a/src/app/views/base/breadcrumbs/breadcrumbs.component.html b/src/app/views/base/breadcrumbs/breadcrumbs.component.html deleted file mode 100644 index af6174f43..000000000 --- a/src/app/views/base/breadcrumbs/breadcrumbs.component.html +++ /dev/null @@ -1,90 +0,0 @@ - - - - - Angular Breadcrumbs - - -

- The breadcrumb navigation provides links back to each previous page the user navigated - through and shows the current location in a website or an application. You don’t have - to add separators, because they automatically added in CSS through - - ::before - - and - - content - - . -

- - - @for (item of items; track item; let i = $index, isLast = $last) { - - {{ item.label }} - - } - -
- - @for (item of items.slice(0, 1); track item; let i = $index, isLast = $last) { - - {{ item.label }} - - } - - - @for (item of items.slice(0, 2); track item; let i = $index, isLast = $last) { - - {{ item.label }} - - } - - - @for (item of items.slice(0, 3); track item; let i = $index, isLast = $last) { - - {{ item.label }} - - } - - - @for (item of items.slice(0, 4); track item; let i = $index, isLast = $last) { - - {{ item.label }} - - } - -
- - - Home - - - Library - - - Data - - - Bootstrap - - -
-
-
-
- - - - Angular Breadcrumbs router - - - - - - - - - -
diff --git a/src/app/views/base/breadcrumbs/breadcrumbs.component.spec.ts b/src/app/views/base/breadcrumbs/breadcrumbs.component.spec.ts deleted file mode 100644 index 075c043bf..000000000 --- a/src/app/views/base/breadcrumbs/breadcrumbs.component.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { BreadcrumbModule, CardModule, GridModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { BreadcrumbsComponent } from './breadcrumbs.component'; - -describe('BreadcrumbsComponent', () => { - let component: BreadcrumbsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [CardModule, GridModule, BreadcrumbModule, RouterTestingModule, BreadcrumbsComponent], - providers: [IconSetService] -}).compileComponents(); - })); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(BreadcrumbsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/base/breadcrumbs/breadcrumbs.component.ts b/src/app/views/base/breadcrumbs/breadcrumbs.component.ts deleted file mode 100644 index e7479e6f5..000000000 --- a/src/app/views/base/breadcrumbs/breadcrumbs.component.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { NgClass } from '@angular/common'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { - BreadcrumbComponent, - BreadcrumbItemComponent, - BreadcrumbRouterComponent, - CardBodyComponent, - CardComponent, - CardHeaderComponent, - ColComponent, - RowComponent, - TextColorDirective -} from '@coreui/angular'; - -@Component({ - templateUrl: './breadcrumbs.component.html', - styleUrls: ['./breadcrumbs.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, BreadcrumbComponent, BreadcrumbItemComponent, NgClass, BreadcrumbRouterComponent] -}) -export class BreadcrumbsComponent implements OnInit { - public items = []; - - constructor() {} - - ngOnInit(): void { - this.items = [ - { label: 'Home', url: '/', attributes: { title: 'Home' } }, - { label: 'Library', url: '/' }, - { label: 'Data', url: '/dashboard/' }, - { label: 'CoreUI', url: '/' } - ]; - - setTimeout(() => { - this.items = [ - { label: 'CoreUI', url: '/' }, - { label: 'Data', url: '/dashboard/' }, - { label: 'Library', url: '/' }, - { label: 'Home', url: '/', attributes: { title: 'Home' } } - ]; - }, 5000); - } -} diff --git a/src/app/views/base/cards/cards.component.html b/src/app/views/base/cards/cards.component.html deleted file mode 100644 index 34083d8ab..000000000 --- a/src/app/views/base/cards/cards.component.html +++ /dev/null @@ -1,873 +0,0 @@ - - - - - Angular Card - - -

- Cards are built with as little markup and styles as possible but still - manage to deliver a bunch of control and customization. Built with - flexbox, they offer easy alignment and mix well with other CoreUI - components. Cards have no top, left, and right margins by default, so - use - spacing utilities - as needed. They have no fixed width to start, so they'll fill the - full width of its parent. -

-

- Below is an example of a basic card with mixed content and a fixed - width. Cards have no fixed width to start, so they'll naturally - fill the full width of its parent element. -

- - - - -
Card title
-

- Some quick example text to build on the card title and make up the bulk of the card's content. -

- -
-
-
-
-
-
- - - - Angular Card Body - - -

- The main block of a card is the <c-card-body>. Use - it whenever you need a padded section within a card. -

- - - This is some text within a card body. - - -
-
-
- - - - Angular Card Titles, text, and links - - -

- Card titles are managed by cCardTitle directive for - <h*>. Identically, links are attached and collected - next to each other by cCardLink directive for - <a> tag. Subtitles are handled by - cCardSubtitle directive. -

-

- Store cCardTitle and the cCardSubtitle items - in a <c-card-body>. The card title, and subtitle - are arranged properly. -

- - - -
Card title
-
- Card subtitle -
-

- Some quick example text to build on the card title and make up the - bulk of the card content. -

- Card link - Another link -
-
-
-
-
-
- - - - Angular Card Images - - -

- cCardImg="top" places a picture to the top of the card. - With cCardText, text can be added to the card. Text - within cCardText can additionally be styled with the - regular HTML tags. -

- - - - -

- Some quick example text to build on the card - title and make up the bulk of the card content. -

-
-
-
-
-
-
- - - - Angular Card list groups - - -

- Create lists of content in a card with a flush list group. -

- - - - -
    -
  • Cras justo odio
  • -
  • Dapibus ac facilisis in
  • -
  • Vestibulum at eros
  • -
-
-
- - - Header -
    -
  • Cras justo odio
  • -
  • Dapibus ac facilisis in
  • -
  • Vestibulum at eros
  • -
-
-
- - -
    -
  • Cras justo odio
  • -
  • Dapibus ac facilisis in
  • -
  • Vestibulum at eros
  • -
- Footer -
-
-
-
-
-
-
- - - - Angular Card kitchen sink - - -

- Combine and match many content types to build the card you need, or - throw everything in there. Shown below are image styles, blocks, text - styles, and a list group—all wrapped in a fixed-width card. -

- - - - -
Card title
-

- Some quick example text to build on the card title and make up the - bulk of the card content. -

-
-
    -
  • Cras justo odio
  • -
  • Dapibus ac facilisis in
  • -
  • Vestibulum at eros
  • -
- - Card link - Another link - -
-
-
-
-
- - - - Card Header and footer - - - - -

- Add an optional header and/or footer within a card. -

- - - Featured - - - - - -
- -

- Card headers can be styled by adding ex. - "h5". -

- - - -
Header
-
- - - -
-
-
-
- - - - - Quote - -
-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. - Integer posuere erat a ante. -

-
- Someone famous in - Source Title -
-
-
-
-
-
- - - - Header - - - - - 2 days ago - - - - -
-
-
-
- - - - Angular Card Sizing - - -

- Cards assume no specific width to start, so they'll - be 100% wide unless otherwise stated. You can adjust this as required - with custom CSS, grid classes, grid Sass mixins, or services. -

-

Using grid markup

-

- Using the grid, wrap cards in columns and rows as needed. -

- - - - - - - - - - - - - - - - - - -

Using utilities

-

- Use some of available sizing utilities to rapidly set a - card width. -

- - - - - - - - - - - - - Using custom CSS -

- Use custom CSS in your stylesheets or as inline styles to set a width. -

- - - - - - - -
-
-
- - - - Card Text alignment - - -

- You can instantly change the text arrangement of any card—in its whole - or specific parts—with - text align - classes. -

- - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- - - - Card Navigation - - -

- Add some navigation to a <c-card-header> with our - <c-nav> component. -

- - - - - - - - @for (tab of tabs; track tab) { - - } - - - - - @for (panel of tabs; track panel) { - - - - } - - - - - - - - - - - - - @for (tab of tabs; track tab) { - - } - - - - - @for (panel of tabs; track panel) { - - - - } - - - - - - - -
-
-
- - - - Card Image caps - - -

- Similar to headers and footers, cards can include top and bottom "image - caps"—images at the top or bottom of a card. -

- - - - - - -
Card title
-

- This is a wider card with supporting text below as a natural lead-in to - additional content. This content is a little bit longer. -

-

- Last updated 3 mins ago -

-
-
-
- - - -
Card title
-

- This is a wider card with supporting text below as a natural lead-in to - additional content. This content is a little bit longer. -

-

- Last updated 3 mins ago -

-
- -
-
-
-
-
-
-
- - - - Card Styles - - -

- Cards include various options for customizing their backgrounds, borders, and color. -

-

Background and color

-

- Use color property to change the appearance of a card. -

- - - @for (item of colors; track item; let i = $index) { - - - Header - -
{{ item.color }} card title
-

- Some quick example text to build on the card title and make up the bulk of - the card's content. -

- -
-
-
- } -
-
-

Border

-

- Use border utilities to change - just the border-color of a card. Note that you can set - textColor property on the <c-card> or a subset of the - card's contents as shown below. -

- - - @for (item of colors; track item; let i = $index) { - - - Header - -
{{ item.color }} card title
-

- Some quick example text to build on the card title and make up the bulk of - the card's content. -

- -
-
-
- } -
-
-

Top border

-

- Use border utilities to change - just the border-color of a card. Note that you can set - textColor property on the <c-card> or a subset of the - card's contents as shown below. -

- - - @for (item of colors; track item; let i = $index) { - - - Header - -
{{ item.color }} card title
-

- Some quick example text to build on the card title and make up the bulk of - the card's content. -

- -
-
-
- } -
-
-
-
-
- - - - Card Card groups - - -

- Use card groups to render cards as a single, attached element with equal width and - height columns. Card groups start off stacked and use display: flex; to - become attached with uniform dimensions starting at the sm breakpoint. -

- - - - - -
Card title
-

- This is a wider card with supporting text below as a natural lead-in to - additional content. This content is a little bit longer. -

-

- Last updated 3 mins ago -

-
-
- - - -
Card title
-

- This card has supporting text below as a natural lead-in to additional - content. -

-

- Last updated 3 mins ago -

-
-
- - - -
Card title
-

- This is a wider card with supporting text below as a natural lead-in to - additional content. This card has even longer content than the first to show - that equal height action. -

-

- Last updated 3 mins ago -

-
-
-
-
-

- When using card groups with footers, their content will automatically line up. -

- - - - - -
Card title
-

- This is a wider card with supporting text below as a natural lead-in to - additional content. This content is a little bit longer. -

-
- - Last updated 3 mins ago - -
- - - -
Card title
-

- This card has supporting text below as a natural lead-in to additional - content. -

-
- - Last updated 3 mins ago - -
- - - -
Card title
-

- This is a wider card with supporting text below as a natural lead-in to - additional content. This card has even longer content than the first to show - that equal height action. -

-
- - Last updated 3 mins ago - -
-
-
-
-
-
- - - - Card Grid cards - - -

- Use the c-row component and set xs|sm|md|lg|xl|xxl property - to control how many grid columns (wrapped around your cards) you show per row. For - example xs="1" laying out the cards on one column, and md="1" splitting - four cards to equal width across multiple rows, from the medium breakpoint up. -

- - - - - - -
Card title
-

- This is a wider card with supporting text below as a natural lead-in to - additional content. This content is a little bit longer. -

-
- - Last updated 3 mins ago - -
-
- - - - -
Card title
-

- This is a wider card with supporting text below as a natural lead-in to - additional content. This content is a little bit longer. -

-
- - Last updated 3 mins ago - -
-
- - - - -
Card title
-

- This is a wider card with supporting text below as a natural lead-in to - additional content. This content is a little bit longer. -

-
- - Last updated 3 mins ago - -
-
- - - - -
Card title
-

- This is a wider card with supporting text below as a natural lead-in to - additional content. This content is a little bit longer. -

-
- - Last updated 3 mins ago - -
-
-
-
-

- Change it to md="3" and you'll see the fourth card wraps. -

- - - - - - -
Card title
-

- This is a wider card with supporting text below as a natural lead-in to - additional content. This content is a little bit longer. -

-
- - Last updated 3 mins ago - -
-
- - - - -
Card title
-

- This is a wider card with supporting text below as a natural lead-in to - additional content. This content is a little bit longer. -

-
- - Last updated 3 mins ago - -
-
- - - - -
Card title
-

- This is a wider card with supporting text below as a natural lead-in to - additional content. This content is a little bit longer. -

-
- - Last updated 3 mins ago - -
-
- - - - -
Card title
-

- This is a wider card with supporting text below as a natural lead-in to - additional content. This content is a little bit longer. -

-
- - Last updated 3 mins ago - -
-
-
-
-
-
-
-
- - - CoreUI for Angular - - - - - Placeholder - - Image cap - - - - -
Card {{ title ?? 'title' }}
-

- Some quick example text to build on the card title and make up the bulk of the card's content. -

- -
- - -
Special title treatment
-

- With supporting text below as a natural lead-in to additional content. -

- -
diff --git a/src/app/views/base/cards/cards.component.scss b/src/app/views/base/cards/cards.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/base/cards/cards.component.spec.ts b/src/app/views/base/cards/cards.component.spec.ts deleted file mode 100644 index c59d01fce..000000000 --- a/src/app/views/base/cards/cards.component.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { provideRouter } from '@angular/router'; - -import { ButtonModule, CardModule, GridModule, ListGroupModule, NavModule, UtilitiesModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { CardsComponent } from './cards.component'; - -describe('CardsComponent', () => { - let component: CardsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CardModule, NavModule, GridModule, ListGroupModule, UtilitiesModule, ButtonModule, CardsComponent, NoopAnimationsModule, ], - providers: [IconSetService, provideRouter([])], - teardown: { destroyAfterEach: false } // <- add this line for Error: NG0205: Injector has already been destroyed. - }) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(CardsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/base/cards/cards.component.ts b/src/app/views/base/cards/cards.component.ts deleted file mode 100644 index 49a1fc4dc..000000000 --- a/src/app/views/base/cards/cards.component.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Component } from '@angular/core'; -import { RouterLink } from '@angular/router'; -import { NgTemplateOutlet } from '@angular/common'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { - BorderDirective, - ButtonDirective, - CardBodyComponent, - CardComponent, - CardFooterComponent, - CardGroupComponent, - CardHeaderComponent, - CardImgDirective, - CardLinkDirective, - CardSubtitleDirective, - CardTextDirective, - CardTitleDirective, - ColComponent, - GutterDirective, - ListGroupDirective, - ListGroupItemDirective, - RowComponent, - TabDirective, - TabPanelComponent, - TabsComponent, - TabsContentComponent, - TabsListComponent, - TextColorDirective -} from '@coreui/angular'; -import { IconDirective } from '@coreui/icons-angular'; - -type CardColor = { - color: string - textColor?: string -} - -@Component({ - selector: 'app-cards', - templateUrl: './cards.component.html', - styleUrls: ['./cards.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, NgTemplateOutlet, CardTitleDirective, CardTextDirective, ButtonDirective, CardSubtitleDirective, CardLinkDirective, RouterLink, ListGroupDirective, ListGroupItemDirective, CardFooterComponent, BorderDirective, CardGroupComponent, GutterDirective, CardImgDirective, TabsComponent, TabsListComponent, IconDirective, TabDirective, TabsContentComponent, TabPanelComponent] -}) -export class CardsComponent { - - colors: CardColor[] = [ - { color: 'primary', textColor: 'primary' }, - { color: 'secondary', textColor: 'secondary' }, - { color: 'success', textColor: 'success' }, - { color: 'danger', textColor: 'danger' }, - { color: 'warning', textColor: 'warning' }, - { color: 'info', textColor: 'info' }, - { color: 'light' }, - { color: 'dark' } - ]; - - imgContext = { $implicit: 'top', bottom: 'bottom' }; - - tabs = ['Active', 'List', 'Disabled'] - -} diff --git a/src/app/views/base/carousels/carousels.component.html b/src/app/views/base/carousels/carousels.component.html deleted file mode 100644 index a6e71f0c8..000000000 --- a/src/app/views/base/carousels/carousels.component.html +++ /dev/null @@ -1,237 +0,0 @@ - - - - - Angular Carousel Slide only - - -

Here’s a carousel with slides

- - - - @for (slide of slides[0]; track slide.src) { - - {{slide.title}} - - } - - - -
-
-
- - - - Angular Carousel with controls - - -

- Adding in the previous and next controls with c-carousel-controls component. -

- - - - @for (slide of slides[0]; track slide.src) { - - {{slide.title}} - - } - - - - - -
-
-
- - - - Angular Carousel with custom controls - - -

- Adding in the previous and next controls with custom content of c-carousel-controls component. -

- - - - @for (slide of slides[0]; track slide.src) { - - {{slide.title}} - - } - - - - Previous - - - - Next - - - -
-
-
- - - - Angular Carousel with indicators - - -

- You can attach the indicators to the carousel, lengthwise the controls, too. -

- - - - - @for (slide of slides[0]; track slide.src) { - - {{slide.title}} - - } - - - -
-
-
- - - - Carousel with captions, controls and indicators - - -

- You can add captions to slides with the <c-carousel-caption> element - within any <c-carousel-item>. They can be immediately hidden on - smaller viewports, as shown below, with optional display - utilities. - We hide them with .d-none and draw them back on medium-sized devices with - .d-md-block. -

- - - - - @for (slide of slides[1]; track slide.src) { - - {{slide.title}} - -

{{ slide.title }}

-

{{ slide.subtitle }}

-
-
- } -
- - -
-
-
-
-
- - - - Angular Carousel Crossfade - - -

- Add transition="crossfade" to your carousel to animate slides - with a fade transition instead of a slide. -

- - - - @for (slide of slides[0]; track slide.src) { - - {{slide.title}} - -

{{ slide.title }}

-

{{ slide.subtitle }}

-
-
- } -
- - -
-
-
-
-
- - - - Angular Carousel Dark variant - - -

- Add dark property to the c-carousel for darker controls, - indicators, and captions. Controls have been inverted from their default white fill - with the filter CSS property. Captions and controls have additional Sass - variables that customize the color and background-color. -

- - - - - @for (slide of slides[2]; track slide.src) { - - {{slide.title}} - -

{{ slide.title }}

-

{{ slide.subtitle }}

-
-
- } -
- - -
-
-
-
-
-
diff --git a/src/app/views/base/carousels/carousels.component.scss b/src/app/views/base/carousels/carousels.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/base/carousels/carousels.component.spec.ts b/src/app/views/base/carousels/carousels.component.spec.ts deleted file mode 100644 index 3af68d27f..000000000 --- a/src/app/views/base/carousels/carousels.component.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { CardModule, CarouselModule, GridModule } from '@coreui/angular'; -import { IconModule } from '@coreui/icons-angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { CarouselsComponent } from './carousels.component'; - -describe('CarouselsComponent', () => { - let component: CarouselsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [CarouselModule, NoopAnimationsModule, CardModule, GridModule, IconModule, RouterTestingModule, CarouselsComponent], - providers: [IconSetService] -}) - .compileComponents(); - })); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(CarouselsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/base/carousels/carousels.component.ts b/src/app/views/base/carousels/carousels.component.ts deleted file mode 100644 index f0b7957c4..000000000 --- a/src/app/views/base/carousels/carousels.component.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Component } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; -import { IconDirective } from '@coreui/icons-angular'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, ThemeDirective, CarouselComponent, CarouselInnerComponent, CarouselItemComponent, CarouselControlComponent, CarouselIndicatorsComponent, CarouselCaptionComponent } from '@coreui/angular'; - -@Component({ - selector: 'app-carousels', - templateUrl: './carousels.component.html', - styleUrls: ['./carousels.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, ThemeDirective, CarouselComponent, CarouselInnerComponent, CarouselItemComponent, CarouselControlComponent, IconDirective, CarouselIndicatorsComponent, CarouselCaptionComponent] -}) -export class CarouselsComponent { - - readonly imageSrc: string[] = [ - 'assets/images/angular.jpg', - 'assets/images/react.jpg', - 'assets/images/vue.jpg', - 'https://picsum.photos/id/1/800/400', - 'https://picsum.photos/id/1026/800/400', - 'https://picsum.photos/id/1031/800/400' - ]; - - readonly slidesLight: string[] = [ - 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22800%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20800%20400%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_1607923e7e2%20text%20%7B%20fill%3A%23AAA%3Bfont-weight%3Anormal%3Bfont-family%3AHelvetica%2C%20monospace%3Bfont-size%3A40pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_1607923e7e2%22%3E%3Crect%20width%3D%22800%22%20height%3D%22400%22%20fill%3D%22%23F5F5F5%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22285.9296875%22%20y%3D%22217.75625%22%3EFirst%20slide%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E', - 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22800%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20800%20400%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_15ba800aa20%20text%20%7B%20fill%3A%23BBB%3Bfont-weight%3Anormal%3Bfont-family%3AHelvetica%2C%20monospace%3Bfont-size%3A40pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_15ba800aa20%22%3E%3Crect%20width%3D%22800%22%20height%3D%22400%22%20fill%3D%22%23EEE%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22247.3203125%22%20y%3D%22218.3%22%3ESecond%20slide%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E', - 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22800%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20800%20400%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_15ba800aa21%20text%20%7B%20fill%3A%23999%3Bfont-weight%3Anormal%3Bfont-family%3AHelvetica%2C%20monospace%3Bfont-size%3A40pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_15ba800aa21%22%3E%3Crect%20width%3D%22800%22%20height%3D%22400%22%20fill%3D%22%23E5E5E5%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22277%22%20y%3D%22218.3%22%3EThird%20slide%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E' - ]; - - readonly slides: any[][] = []; - - constructor( - private domSanitizer: DomSanitizer - ) { - this.slides[0] = [ - { - id: 0, - src: domSanitizer.bypassSecurityTrustUrl(this.imageSrc[0]), - title: 'First slide', - subtitle: 'Nulla vitae elit libero, a pharetra augue mollis interdum.' - }, - { - id: 1, - src: domSanitizer.bypassSecurityTrustUrl(this.imageSrc[1]), - title: 'Second slide', - subtitle: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' - }, - { - id: 2, - src: domSanitizer.bypassSecurityTrustUrl(this.imageSrc[2]), - title: 'Third slide', - subtitle: 'Praesent commodo cursus magna, vel scelerisque nisl consectetur.' - } - ]; - - this.slides[1] = [ - { - id: 0, - src: this.imageSrc[3], - title: 'First slide', - subtitle: 'Nulla vitae elit libero, a pharetra augue mollis interdum.' - }, - { - id: 1, - src: this.imageSrc[4], - title: 'Second slide', - subtitle: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' - }, - { - id: 2, - src: this.imageSrc[5], - title: 'Third slide', - subtitle: 'Praesent commodo cursus magna, vel scelerisque nisl consectetur.' - } - ]; - - this.slides[2] = [ - { - id: 0, - src: domSanitizer.bypassSecurityTrustUrl(this.slidesLight[0]), - title: 'First slide', - subtitle: 'Nulla vitae elit libero, a pharetra augue mollis interdum.' - }, - { - id: 1, - src: domSanitizer.bypassSecurityTrustUrl(this.slidesLight[1]), - title: 'Second slide', - subtitle: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' - }, - { - id: 2, - src: domSanitizer.bypassSecurityTrustUrl(this.slidesLight[2]), - title: 'Third slide', - subtitle: 'Praesent commodo cursus magna, vel scelerisque nisl consectetur.' - } - ]; - - } - - onItemChange($event: any): void { - console.log('Carousel onItemChange', $event); - } - -} diff --git a/src/app/views/base/collapses/collapses.component.html b/src/app/views/base/collapses/collapses.component.html deleted file mode 100644 index d2495a840..000000000 --- a/src/app/views/base/collapses/collapses.component.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - Angular Collapse - - -

You can use a link or a button component.

- - Link - -
- - - Anim pariatur cliche reprehenderit, enim eiusmod high life - accusamus terry richardson ad squid. Nihil anim keffiyeh - helvetica, craft beer labore wes anderson cred nesciunt sapiente - ea proident. - - -
-
-
-
-
- - - - Angular Collapse horizontal - - -

- Add the horizontal property to transition the width - instead of height and set a width on the immediate child element. -

- - -
-
- - - This is some placeholder content for a horizontal collapse. - It's hidden by default and shown when triggered. - - -
-
-
-
-
-
- - - - Angular Collapse multi target - - -

- A <c-button> can show and hide multiple elements. -

- - - - - - -
- - - Anim pariatur cliche reprehenderit, enim eiusmod high life - accusamus terry richardson ad squid. Nihil anim keffiyeh - helvetica, craft beer labore wes anderson cred nesciunt - sapiente ea proident. - - -
-
- -
- - - Anim pariatur cliche reprehenderit, enim eiusmod high life - accusamus terry richardson ad squid. Nihil anim keffiyeh - helvetica, craft beer labore wes anderson cred nesciunt - sapiente ea proident. - - -
-
-
-
-
-
-
-
diff --git a/src/app/views/base/collapses/collapses.component.scss b/src/app/views/base/collapses/collapses.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/base/collapses/collapses.component.spec.ts b/src/app/views/base/collapses/collapses.component.spec.ts deleted file mode 100644 index d22b3571b..000000000 --- a/src/app/views/base/collapses/collapses.component.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { ButtonModule, CardModule, CollapseModule, GridModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { CollapsesComponent } from './collapses.component'; - -describe('CollapsesComponent', () => { - let component: CollapsesComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CardModule, CollapseModule, NoopAnimationsModule, GridModule, ButtonModule, RouterTestingModule, CollapsesComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(CollapsesComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/base/collapses/collapses.component.ts b/src/app/views/base/collapses/collapses.component.ts deleted file mode 100644 index 234676802..000000000 --- a/src/app/views/base/collapses/collapses.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Component } from '@angular/core'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, ButtonDirective, CollapseDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-collapses', - templateUrl: './collapses.component.html', - styleUrls: ['./collapses.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, ButtonDirective, CollapseDirective] -}) -export class CollapsesComponent { - - collapses = [false, false, false, false]; - - constructor() { } - - toggleCollapse(id: number): void { - // @ts-ignore - this.collapses[id] = !this.collapses[id]; - } - -} diff --git a/src/app/views/base/list-groups/list-groups.component.html b/src/app/views/base/list-groups/list-groups.component.html deleted file mode 100644 index 8c7b55d46..000000000 --- a/src/app/views/base/list-groups/list-groups.component.html +++ /dev/null @@ -1,306 +0,0 @@ - - - - - Angular List Group Basic example - - -

- The default list group is an unordered list with items and the proper - CSS classes. Build upon it with the options that follow, or with your - CSS as required. -

- -
    - @for (item of sampleList; track item) { -
  • {{ item }}
  • - } -
-
-
-
-
- - - - Angular List Group Active items - - -

- Add active boolean property to a - cListGroupItem to show the current active selection. -

- -
    - @for (item of sampleList; track item; let first = $first) { -
  • {{ item }}
  • - } -
-
-
-
-
- - - - Angular List Group Disabled items - - -

- Add disabled boolean property to a - cListGroupItem to make it appear disabled. -

- -
- @for (item of sampleList; track item; let first = $first) { - - } -
-
-
-
-
- - - - Angular List Group Links and buttons - - -

- Use <a>s or <button>s to create - actionable list group items with hover, disabled, and active - states with a or button. We - separate these pseudo-classes to ensure list groups made of - non-interactive elements (like <li> or - <div>) don't provide a click or tap affordance. -

- -
- @for (item of sampleList; track item; let first = $first, last = $last) { - {{ item }} - } -
-
-
-
-
- - - - Angular List Group Flush - - -

- Add flush boolean property to remove some borders and - rounded corners to render list group items edge-to-edge in a parent - container (e.g., cards). -

- -
    - @for (item of sampleList; track item) { -
  • {{ item }}
  • - } -
-
-
-
-
- - - - Angular List Group Horizontal - - -

- Add layout="horizontal" to change the layout of - list group items from vertical to horizontal across all breakpoints.
- Alternatively, choose a responsive variant - [horizontal]="sm | md | lg | xl | xxl" - to make a list group horizontal starting at that breakpoint's - min-width.
- Currently horizontal list groups cannot be combined with flush list groups. -

- - @for (breakpoint of breakpoints; track breakpoint) { -
    -
  • Cras justo odio
  • -
  • Dapibus ac facilisis in
  • -
  • Morbi leo risus
  • -
- } -
-
-
-
- - - - Angular List Group Contextual classes - - -

- Use contextual classes to style list items with a stateful background - and color. -

- -
    - @for (color of colors; track color) { -
  • - A simple {{ color }} list group item -
  • - } -
-
-

- Contextual classes also work with <a> or - <button>. Note the addition of the hover styles - here not present in the previous example. Also supported is the - active state; apply it to indicate an active selection on - a contextual list group item. -

- -
- @for (color of colors; track color) { - - A simple {{ color }} list group item - - } -
-
-
-
-
- - - - Angular List Group With badges - - -

- Add badges to any list group item to show unread counts, activity, and - more. -

- -
- @for (item of sampleList; track item; let i = $index, last = $last) { - - } -
-
-
-
-
- - - - Angular List Group Custom content - - -

- Add nearly any HTML within, even for linked list groups like the one - below, with the help of - flexbox utilities. -

- - - -
-
-
- - - - Angular List Group Checkboxes and radios - - -

- Place CoreUI's checkboxes and radios within list group items and - customize as needed. -

- -
-
    - - -
  • - - - - -
  • -
  • - - - - -
  • -
  • - - - - -
  • -
- -
-
-
-
-
-
diff --git a/src/app/views/base/list-groups/list-groups.component.scss b/src/app/views/base/list-groups/list-groups.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/base/list-groups/list-groups.component.spec.ts b/src/app/views/base/list-groups/list-groups.component.spec.ts deleted file mode 100644 index 209b92b93..000000000 --- a/src/app/views/base/list-groups/list-groups.component.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { BadgeModule, ButtonModule, CardModule, FormModule, GridModule, ListGroupModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { ListGroupsComponent } from './list-groups.component'; - -describe('ListGroupsComponent', () => { - let component: ListGroupsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ListGroupModule, ButtonModule, ReactiveFormsModule, BadgeModule, FormModule, GridModule, CardModule, RouterTestingModule, ListGroupsComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(ListGroupsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/base/list-groups/list-groups.component.ts b/src/app/views/base/list-groups/list-groups.component.ts deleted file mode 100644 index 5bced91be..000000000 --- a/src/app/views/base/list-groups/list-groups.component.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Component } from '@angular/core'; -import { UntypedFormBuilder, ReactiveFormsModule } from '@angular/forms'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, ListGroupDirective, ListGroupItemDirective, BadgeComponent, FormDirective, FormCheckComponent, FormCheckInputDirective, FormCheckLabelDirective, ButtonDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-list-groups', - templateUrl: './list-groups.component.html', - styleUrls: ['./list-groups.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, ListGroupDirective, ListGroupItemDirective, BadgeComponent, ReactiveFormsModule, FormDirective, FormCheckComponent, FormCheckInputDirective, FormCheckLabelDirective, ButtonDirective] -}) -export class ListGroupsComponent { - - constructor( - private formBuilder: UntypedFormBuilder - ) { } - - readonly breakpoints: (string | boolean)[] = [true, 'sm', 'md', 'lg', 'xl', 'xxl']; - readonly colors: string[] = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark']; - - readonly checkBoxes = this.formBuilder.group({ - one: false, - two: false, - three: true, - four: true, - five: { value: false, disabled: true } - }); - - readonly sampleList: string[] = [ - 'Cras justo odio', - 'Dapibus ac facilisis in', - 'Morbi leo risus', - 'Porta ac consectetur ac', - 'Vestibulum at eros' - ]; - - setValue(controlName: string) { - const prevValue = this.checkBoxes.get(controlName)?.value; - const value = this.checkBoxes.getRawValue(); - value[controlName] = !prevValue; - this.checkBoxes.setValue(value); - } - - logValue() { - console.log(this.checkBoxes.value); - this.checkBoxes.reset(); - } - - getValue(controlName: string) { - return this.checkBoxes.get(controlName); - } -} diff --git a/src/app/views/base/navs/navs.component.html b/src/app/views/base/navs/navs.component.html deleted file mode 100644 index a9b29270a..000000000 --- a/src/app/views/base/navs/navs.component.html +++ /dev/null @@ -1,416 +0,0 @@ - - - - - Angular Navs Base navs - - -

- The base c-nav component is built with flexbox and provide a strong - foundation for building all types of navigation components. It includes some style - overrides (for working with lists), some link padding for larger hit areas, and basic - [disabled]="true" styling. -

- - - - - Active - - - - Link - - - Link - - - - Disabled - - - - -

- Classes are used throughout, so your markup can be super flexible. Use - c-nav-item like above, or roll your own with a <c-nav> element. Because - the .nav uses display: flex, the cNavLink behaves the same as c-nav-item - would, but - without the extra markup. -

- - - - Active - - Link - Link - - Disabled - - - -
-
-
- - - - Angular Navs Horizontal alignment - - -

- Change the horizontal alignment of your nav with - - flexbox utilities - - . By default, navs are left-aligned, but you can easily change them to center or right - aligned. -

-

- Centered with .justify-content-center: -

- - - - - Active - - - - Link - - - Link - - - - Disabled - - - - -

- Right-aligned with .justify-content-end: -

- - - - - Active - - - - Link - - - Link - - - - Disabled - - - - -
-
-
- - - - Angular Navs Vertical - - -

- Stack your navigation by changing the flex item direction with the - .flex-column utility. Need to stack them on some viewports but not - others? Use the responsive versions (e.g., .flex-sm-column). -

- - - - - Active - - - - Link - - - Link - - - - Disabled - - - - -
-
-
- - - - Angular Navs Tabs - - -

- Take the basic nav from above and add the variant="tabs" class - to generate a tabbed interface -

- - - - - Active - - - - Link - - - Link - - - - Disabled - - - - -
-
-
- - - - Angular Navs Pills - - -

- Take that same HTML, but use variant="pills" instead: -

- - - - - Active - - - - Link - - - Link - - - - Disabled - - - - -
-
-
- - - - Angular Navs Underline - - -

- Take that same HTML, but use variant="Underline" instead: -

- - - - - Active - - - - Link - - - Link - - - - Disabled - - - - -
-
-
- - - - Angular Navs Fill and justify - - -

- Force your .nav's contents to extend the full available width one of - two modifier classes. To proportionately fill all available space with your - .nav-items, use layout="fill". Notice that all - horizontal space is occupied, but not every nav item has the same width. -

- - - - - Active - - - - Link - - - Link - - - - Disabled - - - - -

- For equal-width elements, use layout="justified". All horizontal - space will be occupied by nav links, but unlike the .nav-fill above, every nav item - will be the same width. -

- - - - - Active - - - - Link - - - Link - - - - Disabled - - - - -
-
-
- - - - Angular Navs Working with flex utilities - - -

- If you need responsive nav variations, consider using a series of - flexbox utilities. While more - verbose, these utilities offer greater customization across responsive breakpoints. In - the example below, our nav will be stacked on the lowest breakpoint, then adapt to a - horizontal layout that fills the available width starting from the small breakpoint. -

- - - - Active - - Link - Link - - Disabled - - - -
-
-
- - - - Angular Navs Tabs with dropdowns - - - - - - - - - -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
- - - - - - -
-
-
-
-
- - - - Angular Navs Pills with dropdowns - - - - - - - Active - - - - - Dropdown button - - - - - Link - - - - Disabled - - - - - - - -
diff --git a/src/app/views/base/navs/navs.component.scss b/src/app/views/base/navs/navs.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/base/navs/navs.component.spec.ts b/src/app/views/base/navs/navs.component.spec.ts deleted file mode 100644 index 95c507114..000000000 --- a/src/app/views/base/navs/navs.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { CardModule, DropdownModule, GridModule, NavModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { NavsComponent } from './navs.component'; - -describe('NavsComponent', () => { - let component: NavsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [GridModule, CardModule, RouterTestingModule, NavModule, DropdownModule, NavsComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(NavsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/base/navs/navs.component.ts b/src/app/views/base/navs/navs.component.ts deleted file mode 100644 index 07dbeadab..000000000 --- a/src/app/views/base/navs/navs.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Component } from '@angular/core'; -import { RouterLink } from '@angular/router'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, NavComponent, NavItemComponent, NavLinkDirective, ThemeDirective, DropdownComponent, DropdownToggleDirective, DropdownMenuDirective, DropdownItemDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-navs', - templateUrl: './navs.component.html', - styleUrls: ['./navs.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, NavComponent, NavItemComponent, NavLinkDirective, RouterLink, ThemeDirective, DropdownComponent, DropdownToggleDirective, DropdownMenuDirective, DropdownItemDirective] -}) -export class NavsComponent { - - constructor() { } -} - diff --git a/src/app/views/base/paginations/paginations.component.html b/src/app/views/base/paginations/paginations.component.html deleted file mode 100644 index cfd4c6077..000000000 --- a/src/app/views/base/paginations/paginations.component.html +++ /dev/null @@ -1,218 +0,0 @@ - - - - - Angular Pagination - - -

- We use a large block of connected links for our pagination, making links hard to miss - and easily scalable—all while providing large hit areas. Pagination is built with list - HTML elements so screen readers can announce the number of available links. Use a - wrapping <nav> element to identify it as a navigation section to - screen readers and other assistive technologies. -

-

- In addition, as pages likely have more than one such navigation section, it's - advisable to provide a descriptive aria-label for the - <nav> to reflect its purpose. For example, if the pagination - component is used to navigate between a set of search results, an appropriate label - could be aria-label="Search results pages". -

- - - - Previous - - - 1 - - - 2 - - - 3 - - - Next - - - -
-
-
- - - - Angular Pagination Working with icons - - -

- Looking to use an icon or symbol in place of text for some pagination links? Be sure - to provide proper screen reader support with aria attributes. -

- - - - - - - 1 - - - 2 - - - 3 - - - - - - -
-
-
- - - - Angular Pagination Disabled and active states - - -

- Pagination links are customizable for different circumstances. Use - disabled for links that appear un-clickable and .active to - indicate the current page. -

-

- While the disabled prop uses pointer-events: none to - try to disable the link functionality of <a>s, that CSS - property is not yet standardized and doesn'taccount for keyboard navigation. As - such, we always add tabindex="-1" on disabled links and use - custom JavaScript to fully disable their functionality. -

- - - - - - - 1 - - - 2 - - - 3 - - - - - - -
-
-
- - - - Angular Pagination Sizing - - -

- Fancy larger or smaller pagination? Add sizing="lg" or - sizing="sm" for additional sizes. -

- - - - Previous - - - 1 - - - 2 - - - 3 - - - Next - - - - - - - Previous - - - 1 - - - 2 - - - 3 - - - Next - - - -
-
-
- - - - Angular Pagination Alignment - - -

- Change the alignment of pagination components with - flexbox utilities. -

- - - - Previous - - - 1 - - - 2 - - - 3 - - - Next - - - - - - - Previous - - - 1 - - - 2 - - - 3 - - - Next - - - -
-
-
-
diff --git a/src/app/views/base/paginations/paginations.component.scss b/src/app/views/base/paginations/paginations.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/base/paginations/paginations.component.spec.ts b/src/app/views/base/paginations/paginations.component.spec.ts deleted file mode 100644 index 624f17aa3..000000000 --- a/src/app/views/base/paginations/paginations.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { CardModule, GridModule, PaginationModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { PaginationsComponent } from './paginations.component'; - -describe('PaginationsComponent', () => { - let component: PaginationsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [PaginationModule, CardModule, GridModule, RouterTestingModule, PaginationsComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(PaginationsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/base/paginations/paginations.component.ts b/src/app/views/base/paginations/paginations.component.ts deleted file mode 100644 index e652488cc..000000000 --- a/src/app/views/base/paginations/paginations.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Component } from '@angular/core'; -import { RouterLink } from '@angular/router'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, PaginationComponent, PageItemComponent, PageLinkDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-paginations', - templateUrl: './paginations.component.html', - styleUrls: ['./paginations.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, PaginationComponent, PageItemComponent, PageLinkDirective, RouterLink] -}) -export class PaginationsComponent { - - constructor() { } - -} diff --git a/src/app/views/base/placeholders/placeholders.component.html b/src/app/views/base/placeholders/placeholders.component.html deleted file mode 100644 index 06ef56e4c..000000000 --- a/src/app/views/base/placeholders/placeholders.component.html +++ /dev/null @@ -1,157 +0,0 @@ - - - - - Angular Placeholder - - -

- In the example below, we take a typical card component and recreate it with - placeholders applied to create a "loading card". Size and proportions are the - same between the two. -

- -
- - - -
Card title
-

- Some quick example text to build on the card title and make up the bulk of the - card's content. -

- Go somewhere -
-
- - - Placeholder - - - -
- -
-

- - - - - -

-

- -

-
-
-
-
-
-
- - - Angular Placeholder - - -

- Create placeholders with the cPlaceholder directive and a grid - column cCol directive (e.g., cCol="6") to set the width. They can - replace the text inside an element or be added as a modifier to an existing - component. -

- - - - -
-
- - - Angular Placeholder Width - - -

- You can change the width through grid column classes, width utilities, or - inline styles. -

- - - - - -
-
- - - Angular Placeholder Color - - -

- By default, the cPlaceholder uses currentColor. This - can be overridden with a custom color or utility class. -

- - - - - - - - - - - - -
-
- - - Angular Placeholder Sizing - - -

- The size of cPlaceholders are based on the typographic style of - the parent element. Customize them with size prop: lg, sm, or - xs. -

- - - - - - -
-
- - - Angular Placeholder Animation - - -

- Animate placeholders with cPlaceholderAnimation="glow" or - cPlaceholderAnimation="wave" to better convey the perception of something - being actively loaded. -

- -

- -

- -

- -

-
-
-
-
-
diff --git a/src/app/views/base/placeholders/placeholders.component.scss b/src/app/views/base/placeholders/placeholders.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/base/placeholders/placeholders.component.spec.ts b/src/app/views/base/placeholders/placeholders.component.spec.ts deleted file mode 100644 index e577015e2..000000000 --- a/src/app/views/base/placeholders/placeholders.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { ButtonModule, CardModule, GridModule, UtilitiesModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { PlaceholdersComponent } from './placeholders.component'; - -describe('PlaceholdersComponent', () => { - let component: PlaceholdersComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CardModule, GridModule, UtilitiesModule, ButtonModule, RouterTestingModule, PlaceholdersComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(PlaceholdersComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/base/placeholders/placeholders.component.ts b/src/app/views/base/placeholders/placeholders.component.ts deleted file mode 100644 index 7aa5bee1f..000000000 --- a/src/app/views/base/placeholders/placeholders.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { RouterLink } from '@angular/router'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, CardImgDirective, CardTitleDirective, CardTextDirective, ButtonDirective, ColDirective, PlaceholderAnimationDirective, PlaceholderDirective, BgColorDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-placeholders', - templateUrl: './placeholders.component.html', - styleUrls: ['./placeholders.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, CardImgDirective, CardTitleDirective, CardTextDirective, ButtonDirective, ColDirective, RouterLink, PlaceholderAnimationDirective, PlaceholderDirective, BgColorDirective] -}) -export class PlaceholdersComponent implements OnInit { - - constructor() { } - - ngOnInit(): void { - } - -} diff --git a/src/app/views/base/popovers/popovers.component.html b/src/app/views/base/popovers/popovers.component.html deleted file mode 100644 index bf9327673..000000000 --- a/src/app/views/base/popovers/popovers.component.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - - Angular Popover Basic example - - - - - - - - - - - - Angular Popover Four directions - - -

- Four options are available: top, right, bottom, and left aligned. -

- - - - - - -
-
-
-
diff --git a/src/app/views/base/popovers/popovers.component.scss b/src/app/views/base/popovers/popovers.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/base/popovers/popovers.component.spec.ts b/src/app/views/base/popovers/popovers.component.spec.ts deleted file mode 100644 index 1b1f127d8..000000000 --- a/src/app/views/base/popovers/popovers.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { ButtonModule, CardModule, GridModule, PopoverModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { PopoversComponent } from './popovers.component'; - -describe('PopoversComponent', () => { - let component: PopoversComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CardModule, GridModule, PopoverModule, ButtonModule, RouterTestingModule, PopoversComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(PopoversComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/base/popovers/popovers.component.ts b/src/app/views/base/popovers/popovers.component.ts deleted file mode 100644 index b0a4601bf..000000000 --- a/src/app/views/base/popovers/popovers.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, ButtonDirective, PopoverDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-popovers', - templateUrl: './popovers.component.html', - styleUrls: ['./popovers.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, ButtonDirective, PopoverDirective] -}) -export class PopoversComponent implements OnInit { - - visible = true; - - constructor() { } - - ngOnInit(): void { - setTimeout(() => { - this.visible = !this.visible; - }, 3000); - } - -} diff --git a/src/app/views/base/progress/progress.component.html b/src/app/views/base/progress/progress.component.html deleted file mode 100644 index d020376cf..000000000 --- a/src/app/views/base/progress/progress.component.html +++ /dev/null @@ -1,157 +0,0 @@ - - - - - Angular Progress Basic example - - -

- Progress components are built with two HTML elements, some CSS to set the width, and a - few attributes. We don't use - - the HTML5 <progress> element - - , ensuring you can stack progress bars, animate them, and place text labels over them. -

- - - - - - {{ value }}% -
- - 33% - - -
-
-
-
- - - - Angular Progress Labels - - -

- Add labels to your progress bars by placing text as - <c-progress> content. -

- - 25% - -
-
-
- - - - Angular Progress Height - - -

- We only set a height value on the <c-progress>, so if - you change that value, the inner <c-progress-bar> will automatically - resize accordingly. -

- - - - - - - - - - - - -
-
-
- - - - Angular Progress Backgrounds - - -

- Use color prop to change the appearance of individual progress bars. -

- - - - - - -
-
-
- - - - Angular Progress Multiple bars - - -

- Include multiple progress bars in a progress component if you need. -

- - - 15% - 30% - - 20% - - -
- - 15 - 30 - 20 - -
-
-
-
- - - - Angular Progress Striped - - -

- Add variant="striped" to any <c-progress> to - apply a stripe via CSS gradient over the progress bar's background color. -

- - - - - - -
-
-
- - - - Angular Progress Animated stripes - - -

- The striped gradient can also be animated. Add [animated]="true" property to - <c-progress> to animate the stripes right to left via CSS3 - animations. -

- - - - - - -
-
-
-
diff --git a/src/app/views/base/progress/progress.component.spec.ts b/src/app/views/base/progress/progress.component.spec.ts deleted file mode 100644 index 28993968e..000000000 --- a/src/app/views/base/progress/progress.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { CardModule, GridModule, ProgressModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { ProgressComponent } from './progress.component'; - -describe('ProgressComponent', () => { - let component: ProgressComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ProgressModule, CardModule, GridModule, RouterTestingModule, ProgressComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(ProgressComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/base/progress/progress.component.ts b/src/app/views/base/progress/progress.component.ts deleted file mode 100644 index 9b1ea2ce5..000000000 --- a/src/app/views/base/progress/progress.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Component } from '@angular/core'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, ProgressBarDirective, ProgressComponent as ProgressComponent_1, ProgressBarComponent, ProgressStackedComponent } from '@coreui/angular'; - -@Component({ - selector: 'app-progress', - templateUrl: './progress.component.html', - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, ProgressBarDirective, ProgressComponent_1, ProgressBarComponent, ProgressStackedComponent] -}) -export class ProgressComponent { - - value = 10; - variant?: 'striped'; - - constructor() { - setTimeout(() => { - this.value = 100; - this.variant = 'striped'; - }, 3000); - } - -} diff --git a/src/app/views/base/routes.ts b/src/app/views/base/routes.ts deleted file mode 100644 index 3bb2e62c4..000000000 --- a/src/app/views/base/routes.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { Routes } from '@angular/router'; - -export const routes: Routes = [ - { - path: '', - data: { - title: 'Base' - }, - children: [ - { - path: '', - redirectTo: 'cards', - pathMatch: 'full' - }, - { - path: 'accordion', - loadComponent: () => import('./accordion/accordions.component').then(m => m.AccordionsComponent), - data: { - title: 'Accordion' - } - }, - { - path: 'breadcrumbs', - loadComponent: () => import('./breadcrumbs/breadcrumbs.component').then(m => m.BreadcrumbsComponent), - data: { - title: 'Breadcrumbs' - } - }, - { - path: 'cards', - loadComponent: () => import('./cards/cards.component').then(m => m.CardsComponent), - data: { - title: 'Cards' - } - }, - { - path: 'carousel', - loadComponent: () => import('./carousels/carousels.component').then(m => m.CarouselsComponent), - data: { - title: 'Carousel' - } - }, - { - path: 'collapse', - loadComponent: () => import('./collapses/collapses.component').then(m => m.CollapsesComponent), - data: { - title: 'Collapse' - } - }, - { - path: 'list-group', - loadComponent: () => import('./list-groups/list-groups.component').then(m => m.ListGroupsComponent), - data: { - title: 'List Group' - } - }, - { - path: 'navs', - loadComponent: () => import('./navs/navs.component').then(m => m.NavsComponent), - data: { - title: 'Navs & Tabs' - } - }, - { - path: 'pagination', - loadComponent: () => import('./paginations/paginations.component').then(m => m.PaginationsComponent), - data: { - title: 'Pagination' - } - }, - { - path: 'placeholder', - loadComponent: () => import('./placeholders/placeholders.component').then(m => m.PlaceholdersComponent), - data: { - title: 'Placeholder' - } - }, - { - path: 'popovers', - loadComponent: () => import('./popovers/popovers.component').then(m => m.PopoversComponent), - data: { - title: 'Popovers' - } - }, - { - path: 'progress', - loadComponent: () => import('./progress/progress.component').then(m => m.ProgressComponent), - data: { - title: 'Progress' - } - }, - { - path: 'spinners', - loadComponent: () => import('./spinners/spinners.component').then(m => m.SpinnersComponent), - data: { - title: 'Spinners' - } - }, - { - path: 'tables', - loadComponent: () => import('./tables/tables.component').then(m => m.TablesComponent), - data: { - title: 'Tables' - } - }, - { - path: 'tabs', - loadComponent: () => import('./tabs/tabs.component').then(m => m.AppTabsComponent), - data: { - title: 'Tabs' - } - }, - { - path: 'tooltips', - loadComponent: () => import('./tooltips/tooltips.component').then(m => m.TooltipsComponent), - data: { - title: 'Tooltips' - } - } - ] - } -]; - - diff --git a/src/app/views/base/spinners/spinners.component.html b/src/app/views/base/spinners/spinners.component.html deleted file mode 100644 index 6884f3d36..000000000 --- a/src/app/views/base/spinners/spinners.component.html +++ /dev/null @@ -1,111 +0,0 @@ - - - - - Angular Spinner Border - - -

- Use the border spinners for a lightweight loading indicator. -

- - - -

- The border spinner uses currentColor for its border-color. - You can use any of our text color utilities on the standard spinner. -

- - - - - - - - - - -
-
-
- - - - Angular Spinner Growing - - -

- If you don'tfancy a border spinner, switch to the grow spinner. While it - doesn't technically spin, it does repeatedly grow! -

- - - -

- Once again, this spinner is built with currentColor, so you can easily - change its appearance. Here it is in blue, along with the supported variants. -

- - - - - - - - - - -
-
-
- - - - Angular Spinner Size - - -

- Add size="sm" property to make a smaller spinner that can quickly - be used within other components. -

- - - - -
-
-
- - - - Angular Spinner Buttons - - -

- Use spinners within buttons to indicate an action is currently processing or taking - place. You may also swap the text out of the spinner element and utilize button text - as needed. -

- - - - - -
- - -
-
-
-
-
diff --git a/src/app/views/base/spinners/spinners.component.scss b/src/app/views/base/spinners/spinners.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/base/spinners/spinners.component.spec.ts b/src/app/views/base/spinners/spinners.component.spec.ts deleted file mode 100644 index 3677f7101..000000000 --- a/src/app/views/base/spinners/spinners.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { ButtonModule, CardModule, GridModule, SpinnerModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { SpinnersComponent } from './spinners.component'; - -describe('SpinnersComponent', () => { - let component: SpinnersComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [SpinnerModule, CardModule, GridModule, ButtonModule, RouterTestingModule, SpinnersComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(SpinnersComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/base/spinners/spinners.component.ts b/src/app/views/base/spinners/spinners.component.ts deleted file mode 100644 index d107bb83b..000000000 --- a/src/app/views/base/spinners/spinners.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component } from '@angular/core'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, SpinnerComponent, ButtonDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-spinners', - templateUrl: './spinners.component.html', - styleUrls: ['./spinners.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, SpinnerComponent, ButtonDirective] -}) -export class SpinnersComponent { - - constructor() { } - -} diff --git a/src/app/views/base/tables/tables.component.html b/src/app/views/base/tables/tables.component.html deleted file mode 100644 index 797fb3c60..000000000 --- a/src/app/views/base/tables/tables.component.html +++ /dev/null @@ -1,1004 +0,0 @@ - - - - - Angular Table Basic example - - -

- Using the most basic table CoreUI, here's how cTable-based - tables look in CoreUI. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
-
-
-
-
- - - - Angular Table Variants - - -

- Use contextual classes to color tables, table rows or individual cells. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ClassHeadingHeading
DefaultCellCell
PrimaryCellCell
SecondaryCellCell
SuccessCellCell
DangerCellCell
WarningCellCell
InfoCellCell
LightCellCell
DarkCellCell
-
-
-
-
- - - - Angular Table Striped rows - - -

- Use striped property to add zebra-striping to any table row within the <tbody>. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
-
-

- These classes can also be added to table variants: -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
-
-
-
-
- - - - Angular Table Striped columns - - -

- Use stripedColumn property to add zebra-striping to any table column. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
-
-
-
-
- - - - Angular Table Hoverable rows - - -

- Use [hover]="true" property to enable a hover state on table rows within a - <tbody>. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
-
-
-
-
- - - - Angular Table Active tables - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3 - Larry the Bird - @twitter
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3 - Larry the Bird - @twitter
-
-
-
-
- - - - Angular Table Bordered tables - - -

- Add bordered property for borders on all sides of the table and cells. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
-
-

- - Border color utilities - can be added to change colors: -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
-
-
-
-
- - - - Angular Table Tables without borders - - -

- Add borderless property for a table without borders. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
-
-
-
-
- - - - Angular Table Small tables - - -

- Add small property to make any cTable more compact - by cutting all cell padding in half. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
-
-
-
-
- - - - Angular Table Vertical alignment - - -

- Table cells of <thead> are always vertical aligned to the - bottom. Table cells in <tbody> inherit their alignment from - cTable and are aligned to the the top by default. Use the align - property to re-align where needed. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Heading 1 - - Heading 2 - - Heading 3 - - Heading 4 -
- This cell inherits vertical-align: middle; from the table - - This cell inherits vertical-align: middle; from the table - - This cell inherits vertical-align: middle; from the table - - This here is some placeholder text, intended to take up quite a bit of - vertical space, to demonstrate how the vertical alignment works in the - preceding cells. -
- This cell inherits vertical-align: bottom; from the table row - - This cell inherits vertical-align: bottom; from the table row - - This cell inherits vertical-align: bottom; from the table row - - This here is some placeholder text, intended to take up quite a bit of - vertical space, to demonstrate how the vertical alignment works in the - preceding cells. -
- This cell inherits vertical-align: middle; from the table - - This cell inherits vertical-align: middle; from the table - This cell is aligned to the top. - This here is some placeholder text, intended to take up quite a bit of - vertical space, to demonsCTableRowate how the vertical alignment works in the - preceding cells. -
-
-
-
-
- - - - Angular Table Nesting - - -

- Border styles, active styles, and table variants are not inherited by nested tables. -

- - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
- - - - - - - - - - - - - - - - - - - - - - - - - -
HeaderHeaderHeader
AFirstLast
BFirstLast
CFirstLast
-
3Larry the Bird@twitter
-
-
-
-
- - - - Angular Table Table head - - -

- Similar to tables and dark tables, use the modifier prop - color="light" or color="dark" to make - <thead>s appear light or dark gray. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3Larrythe Bird@twitter
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
-
-
-
-
- - - - Angular Table Table foot - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
FooterFooterFooterFooter
-
-
-
-
- - - - Angular Table Captions - - -

- A <caption> functions like a heading for a table. It helps - users with screen readers to find a table and understand what it's about and - decide if they want to read it. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
List of users
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3Larrythe Bird@twitter
-
-

- You can also put the <caption> on the top of the table with - caption="top". -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
List of users
#ClassHeadingHeading
1MarkOtto@mdo
2JacobThornton@fat
3Larrythe Bird@twitter
-
-
-
-
-
diff --git a/src/app/views/base/tables/tables.component.scss b/src/app/views/base/tables/tables.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/base/tables/tables.component.spec.ts b/src/app/views/base/tables/tables.component.spec.ts deleted file mode 100644 index 611060efa..000000000 --- a/src/app/views/base/tables/tables.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { CardModule, GridModule, TableModule, UtilitiesModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { TablesComponent } from './tables.component'; - -describe('TablesComponent', () => { - let component: TablesComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [GridModule, CardModule, TableModule, GridModule, UtilitiesModule, RouterTestingModule, TablesComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(TablesComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/base/tables/tables.component.ts b/src/app/views/base/tables/tables.component.ts deleted file mode 100644 index f8732a68f..000000000 --- a/src/app/views/base/tables/tables.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component } from '@angular/core'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, TableDirective, TableColorDirective, TableActiveDirective, BorderDirective, AlignDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-tables', - templateUrl: './tables.component.html', - styleUrls: ['./tables.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, TableDirective, TableColorDirective, TableActiveDirective, BorderDirective, AlignDirective] -}) -export class TablesComponent { - - constructor() { } - -} diff --git a/src/app/views/base/tabs/tabs.component.html b/src/app/views/base/tabs/tabs.component.html deleted file mode 100644 index 5e74b0195..000000000 --- a/src/app/views/base/tabs/tabs.component.html +++ /dev/null @@ -1,115 +0,0 @@ - - - - - Angular Tabs underline - - - - - - - - - - - This is some placeholder content the Home tab's associated content. Clicking another tab - will toggle the visibility of this one for the next. The tab JavaScript swaps classes to control the - content visibility and styling. You can use it with tabs, pills, and any other .nav-powered navigation. - - - This is some placeholder content the Profile tab's associated content. Clicking another - tab will toggle the visibility of this one for the next. The tab JavaScript swaps classes to control the - content visibility and styling. You can use it with tabs, pills, and any other .nav-powered navigation. - - - This is some placeholder content the Contact tab's associated content. Clicking another - tab will toggle the visibility of this one for the next. The tab JavaScript swaps classes to control the - content visibility and styling. You can use it with tabs, pills, and any other .nav-powered navigation. - - - - - - - - - - Angular Tabs tabs - - - - - @for (tab of panes; track i; let i = $index, isLast = $last) { - - } - - - @for (pane of panes; track i; let i = $index) { - - This is some placeholder content the {{ pane.name }} tab's associated content. Clicking - another tab will toggle the visibility of this one for the next. The tab JavaScript swaps classes to - control the content visibility and styling. You can use it with tabs, pills, and any other .nav-powered - navigation. - - } - - - - - - - - - Angular Tabs pills - - - - - - - - - - - This is some placeholder content the Home tab's associated content. Clicking another tab - will toggle the visibility of this one for the next. The tab JavaScript swaps classes to control the - content visibility and styling. You can use it with tabs, pills, and any other .nav-powered navigation. - - - This is some placeholder content the Profile tab's associated content. Clicking another - tab will toggle the visibility of this one for the next. The tab JavaScript swaps classes to control the - content visibility and styling. You can use it with tabs, pills, and any other .nav-powered navigation. - - - This is some placeholder content the Contact tab's associated content. Clicking another - tab will toggle the visibility of this one for the next. The tab JavaScript swaps classes to control the - content visibility and styling. You can use it with tabs, pills, and any other .nav-powered navigation. - - - - - - - diff --git a/src/app/views/base/tabs/tabs.component.scss b/src/app/views/base/tabs/tabs.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/base/tabs/tabs.component.spec.ts b/src/app/views/base/tabs/tabs.component.spec.ts deleted file mode 100644 index 84f129320..000000000 --- a/src/app/views/base/tabs/tabs.component.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; - -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { AppTabsComponent } from './tabs.component'; - -describe('TabsComponent', () => { - let component: AppTabsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [AppTabsComponent, NoopAnimationsModule], - providers: [IconSetService], - teardown: { destroyAfterEach: false } // <- add this line for Error: NG0205: Injector has already been destroyed. - }).compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(AppTabsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/base/tabs/tabs.component.ts b/src/app/views/base/tabs/tabs.component.ts deleted file mode 100644 index a24bb5fbd..000000000 --- a/src/app/views/base/tabs/tabs.component.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Component, signal } from '@angular/core'; -import { - CardBodyComponent, - CardComponent, - CardHeaderComponent, - ColComponent, - RoundedDirective, - RowComponent, - TabDirective, - TabPanelComponent, - TabsComponent, - TabsContentComponent, - TabsListComponent -} from '@coreui/angular'; -import { IconDirective } from '@coreui/icons-angular'; - -@Component({ - selector: 'app-tabs', - templateUrl: './tabs.component.html', - styleUrls: ['./tabs.component.scss'], - standalone: true, - imports: [ - CardBodyComponent, - CardComponent, - CardHeaderComponent, - ColComponent, - RoundedDirective, - RowComponent, - TabDirective, - TabPanelComponent, - TabsComponent, - TabsContentComponent, - TabsListComponent, - IconDirective - ] -}) -export class AppTabsComponent { - - public panes = [ - { name: 'Home 01', id: 'tab-01', icon: 'cilHome' }, - { name: 'Profile 02', id: 'tab-02', icon: 'cilUser' }, - { name: 'Contact 03', id: 'tab-03', icon: 'cilCode' } - ]; - - activeItem = signal(0); - - handleActiveItemChange(value: string | number | undefined) { - this.activeItem.set(value); - } -} diff --git a/src/app/views/base/tooltips/tooltips.component.html b/src/app/views/base/tooltips/tooltips.component.html deleted file mode 100644 index e9ee84b27..000000000 --- a/src/app/views/base/tooltips/tooltips.component.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - Angular Tooltip Basic example - - -

- Hover over the links below to see tooltips: -

- -

- Tight pants next level keffiyeh - you probably - haven'theard of them. Photo booth beard raw denim letterpress vegan messenger - bag stumptown. Farm-to-table seitan, mcsweeney's fixie sustainable quinoa 8-bit - american apparel - have a - terry richardson vinyl chambray. Beard stumptown, cardigans banh mi lomo - thundercats. Tofu biodiesel williamsburg marfa, four loko mcsweeney''s - cleanse vegan chambray. A really ironic artisan - whatever keytar - scenester farm-to-table banksy Austin - twitter handle - freegan cred raw denim single-origin coffee viral. -

-
-

- Hover over the buttons below to see the four tooltips directions: top, right, bottom, - and left. -

- - - - - - -
-
-
-
diff --git a/src/app/views/base/tooltips/tooltips.component.scss b/src/app/views/base/tooltips/tooltips.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/base/tooltips/tooltips.component.spec.ts b/src/app/views/base/tooltips/tooltips.component.spec.ts deleted file mode 100644 index 049215d9f..000000000 --- a/src/app/views/base/tooltips/tooltips.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { ButtonModule, CardModule, GridModule, TooltipModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { TooltipsComponent } from './tooltips.component'; - -describe('TooltipsComponent', () => { - let component: TooltipsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CardModule, GridModule, TooltipModule, ButtonModule, RouterTestingModule, TooltipsComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(TooltipsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/base/tooltips/tooltips.component.ts b/src/app/views/base/tooltips/tooltips.component.ts deleted file mode 100644 index 376fe1ec1..000000000 --- a/src/app/views/base/tooltips/tooltips.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Component } from '@angular/core'; -import { RouterLink } from '@angular/router'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, TooltipDirective, ButtonDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-tooltips', - templateUrl: './tooltips.component.html', - styleUrls: ['./tooltips.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, TooltipDirective, RouterLink, ButtonDirective] -}) -export class TooltipsComponent { - - constructor() { } - -} diff --git a/src/app/views/buttons/button-groups/button-groups.component.html b/src/app/views/buttons/button-groups/button-groups.component.html deleted file mode 100644 index f3820441e..000000000 --- a/src/app/views/buttons/button-groups/button-groups.component.html +++ /dev/null @@ -1,394 +0,0 @@ - - - - - Angular Button Group Basic example - - -

- Wrap a series of <CButton> components in - <c-button-group>. -

- - - - - - - -

- These classes can also be added to groups of links, as an alternative to the - <CNav> components. -

- - - - Active link - - - Link - - - Link - - - -
-
-
- - - - Angular Button Group Mixed styles - - - - - - - - - - - - - - - - Angular Button Group Outlined styles - - - - - - - - - - - - - - - - Angular Button Group Checkbox and radio button groups - - -

- Combine button-like checkbox and radio toggle buttons into a seamless looking button - group. -

- -
- - - - - - - - - - -
-
-
- -
- - - - - - - - - - -
-
-
-
-
- - - - Angular Button Group Button toolbar - - -

- Join sets of button groups into button toolbars for more complicated components. Use - utility classes as needed to space out groups, buttons, and more. -

- - - - - - - - - - - - - - - - - - -

- Feel free to combine input groups with button groups in your toolbars. Similar to the - example above, you’ll likely need some utilities through to space items correctly. -

- - - - - - - - - - @ - - - - - - - - - - - - @ - - - - -
-
-
- - - - Angular Button Group Sizing - - -

- Alternatively, of implementing button sizing classes to each button in a group, set - size property to all <c-button-group>'s, including - each one when nesting multiple groups. -

- - - - - - -
- - - - - -
- - - - - -
-
-
-
- - - - Angular Button Group Nesting - - -

- Put a <c-button-group> inside another - <c-button-group> when you need dropdown menus combined with a series - of buttons. -

- - - - - - - - - - -
-
-
- - - - Angular Button Group Vertical variation - - -

- Create a set of buttons that appear vertically stacked rather than horizontally. - Split button dropdowns are not supported here. -

- - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - -
-
-
-
-
-
diff --git a/src/app/views/buttons/button-groups/button-groups.component.scss b/src/app/views/buttons/button-groups/button-groups.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/buttons/button-groups/button-groups.component.spec.ts b/src/app/views/buttons/button-groups/button-groups.component.spec.ts deleted file mode 100644 index ea5d77290..000000000 --- a/src/app/views/buttons/button-groups/button-groups.component.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; -import { ReactiveFormsModule } from '@angular/forms'; - -import { ButtonGroupModule, ButtonModule, CardModule, DropdownModule, FormModule, GridModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { ButtonGroupsComponent } from './button-groups.component'; - -describe('ButtonGroupsComponent', () => { - let component: ButtonGroupsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ReactiveFormsModule, ButtonModule, DropdownModule, FormModule, GridModule, CardModule, RouterTestingModule, ButtonModule, ButtonGroupModule, ButtonGroupsComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(ButtonGroupsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/buttons/button-groups/button-groups.component.ts b/src/app/views/buttons/button-groups/button-groups.component.ts deleted file mode 100644 index 22d065bd4..000000000 --- a/src/app/views/buttons/button-groups/button-groups.component.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Component } from '@angular/core'; -import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, ReactiveFormsModule } from '@angular/forms'; -import { RouterLink } from '@angular/router'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, ButtonGroupComponent, ButtonDirective, FormCheckLabelDirective, ButtonToolbarComponent, InputGroupComponent, InputGroupTextDirective, FormControlDirective, ThemeDirective, DropdownComponent, DropdownToggleDirective, DropdownMenuDirective, DropdownItemDirective, DropdownDividerDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-button-groups', - templateUrl: './button-groups.component.html', - styleUrls: ['./button-groups.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, ButtonGroupComponent, ButtonDirective, RouterLink, ReactiveFormsModule, FormCheckLabelDirective, ButtonToolbarComponent, InputGroupComponent, InputGroupTextDirective, FormControlDirective, ThemeDirective, DropdownComponent, DropdownToggleDirective, DropdownMenuDirective, DropdownItemDirective, DropdownDividerDirective] -}) -export class ButtonGroupsComponent { - - formCheck1 = this.formBuilder.group({ - checkbox1: false, - checkbox2: false, - checkbox3: false - }); - formRadio1 = new UntypedFormGroup({ - radio1: new UntypedFormControl('Radio1') - }); - - constructor( - private formBuilder: UntypedFormBuilder - ) { } - - setCheckBoxValue(controlName: string) { - const prevValue = this.formCheck1.get(controlName)?.value; - const value = this.formCheck1.value; - value[controlName] = !prevValue; - this.formCheck1.setValue(value); - } - - setRadioValue(value: string): void { - this.formRadio1.setValue({ radio1: value }); - } -} diff --git a/src/app/views/buttons/buttons/buttons.component.html b/src/app/views/buttons/buttons/buttons.component.html deleted file mode 100644 index 887a898ec..000000000 --- a/src/app/views/buttons/buttons/buttons.component.html +++ /dev/null @@ -1,347 +0,0 @@ - - - - - Angular Button - - -

- CoreUI includes a bunch of predefined buttons components, each serving its own - semantic purpose. Buttons show what action will happen when the user clicks or touches - it. CoreUI buttons are used to initialize operations, both in the background or - foreground of an experience. -

- - @for (state of states; track state; let i = $index) { - - - {{ state.charAt(0).toUpperCase() + state.slice(1) }} - - - @for (color of colors; track color; let i = $index) { - - } - - - - } - -
-
-
- - - - Angular Button with icons - - -

- You can combine button with our CoreUI Icons. -

- - @for (state of states; track state; let i = $index) { - - - {{ state.charAt(0).toUpperCase() + state.slice(1) }} - - - @for (color of colors; track color; let i = $index) { - - } - - - - } - -
-
-
- - - - Angular Button Button components - - -

- The <button> component are designed for - <button> , <a> or <input> - elements (though some browsers may apply a slightly different rendering). -

-

- If you're using <button> component as <a> - elements that are used to trigger functionality ex. collapsing content, these links - should be given a role="button" to adequately communicate their - meaning to assistive technologies such as screen readers. -

- - - Link - - - - - - -
-
-
- - - - Angular Button outline - - -

- If you need a button, but without the strong background colors. Set - variant="outline" prop to remove all background colors. -

- - @for (state of states; track state; let i = $index) { - - - {{ state.charAt(0).toUpperCase() + state.slice(1) }} - - - @for (color of colors; track color; let i = $index) { - - } - - - } - -
-
-
- - - - Angular Button ghost - - -

- If you need a ghost variant of button, set variant="ghost" prop - to remove all background colors. -

- - @for (state of states; track state; let i = $index) { - - - {{ state.charAt(0).toUpperCase() + state.slice(1) }} - - - @for (color of colors; track color; let i = $index) { - - } - - - } - -
-
-
- - - - Angular Button Sizes - - -

- Larger or smaller buttons? Add size="lg" - size="sm" for additional sizes. -

- - - - - -
- - -
-
-
-
- - - - Angular Button Pill - - - - @for (color of colors; track color; let i = $index) { - - } - - - - - - - - Angular Button Square - - - - @for (color of colors; track color; let i = $index) { - - } - - - - - - - - Angular Button Disabled state - - -

- Add the disabled boolean prop to any <button> - component to make buttons look inactive. Disabled button has - pointer-events: none applied to, disabling hover and active states from - triggering. -

- - - - -

- Disabled buttons using the <a> component act a little different: -

-

- <a>s don'tsupport the disabled attribute, so - CoreUI has to add .disabled class to make buttons look inactive. - CoreUI also has to add to the disabled button component - aria-disabled="true" attribute to show the state of the component - to assistive technologies. -

- - - Primary link - - - Link - - -
-
-
- - - - Angular Button Block buttons - - -

- Create buttons that span the full width of a parent—by using utilities. -

- -
- - -
-
-

- Here we create a responsive variation, starting with vertically stacked buttons until - the md breakpoint, where .d-md-block replaces the - .d-grid class, thus nullifying the gap-2 utility. Resize - your browser to see them change. -

- -
- - -
-
-

- You can adjust the width of your block buttons with grid column width classes. For - example, for a half-width "block button", use .col-6. Center it - horizontally with .mx-auto, too. -

- -
- - -
-
-

- Additional utilities can be used to adjust the alignment of buttons when horizontal. - Here we've taken our previous responsive example and added some flex utilities and - a margin utility on the button to right align the buttons when they're no longer - stacked. -

- -
- - -
-
-
-
-
-
diff --git a/src/app/views/buttons/buttons/buttons.component.spec.ts b/src/app/views/buttons/buttons/buttons.component.spec.ts deleted file mode 100644 index 8cad0727c..000000000 --- a/src/app/views/buttons/buttons/buttons.component.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { ButtonModule, CardModule, GridModule } from '@coreui/angular'; -import { IconModule } from '@coreui/icons-angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { ButtonsComponent } from './buttons.component'; - -describe('ButtonsComponent', () => { - let component: ButtonsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CardModule, GridModule, ButtonModule, RouterTestingModule, IconModule, ButtonsComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(ButtonsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/buttons/buttons/buttons.component.ts b/src/app/views/buttons/buttons/buttons.component.ts deleted file mode 100644 index f373624d7..000000000 --- a/src/app/views/buttons/buttons/buttons.component.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Component } from '@angular/core'; -import { RouterLink } from '@angular/router'; -import { IconDirective } from '@coreui/icons-angular'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { - ButtonDirective, - CardBodyComponent, - CardComponent, - CardHeaderComponent, - ColComponent, - RowComponent, - TextColorDirective -} from '@coreui/angular'; - -@Component({ - selector: 'app-buttons', - templateUrl: './buttons.component.html', - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, ButtonDirective, IconDirective, RouterLink] -}) -export class ButtonsComponent { - - states = ['normal', 'active', 'disabled']; - colors = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark']; - -} diff --git a/src/app/views/buttons/dropdowns/dropdowns.component.html b/src/app/views/buttons/dropdowns/dropdowns.component.html deleted file mode 100644 index bdafa6839..000000000 --- a/src/app/views/buttons/dropdowns/dropdowns.component.html +++ /dev/null @@ -1,524 +0,0 @@ - - - - - Angular Dropdown Single button - - -

- Here's how you can put them to work with either <button> - elements: -

- - - - - - -

- The best part is you can do this with any button variant, too: -

- - @for (color of colors; track color; let i = $index) { - - -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • -
    -
  • -
  • - -
  • -
-
- } -
-
-
-
- - - - Angular Dropdown Split button - - -

- Similarly, create split button dropdowns with virtually the same markup as single - button dropdowns, but with the addition of boolean prop split for proper - spacing around the dropdown caret. -

-

- We use this extra class to reduce the horizontal padding on either side - of the caret by 25% and remove the margin-left that's attached for - normal button dropdowns. Those additional changes hold the caret centered in the split - button and implement a more properly sized hit area next to the main button. -

- - @for (color of colors; track color; let i = $index) { - - - -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • -
    -
  • -
  • - -
  • -
-
- } -
-
-
-
- - - - Angular Dropdown Sizing - - -

- Button dropdowns work with buttons of all sizes, including default and split dropdown - buttons. -

- - - -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • -
    -
  • -
  • - -
  • -
-
- - - -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • -
    -
  • -
  • - -
  • -
-
-
- - - -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • -
    -
  • -
  • - -
  • -
-
- - - -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • -
    -
  • -
  • - -
  • -
-
-
-
-
-
- - - - Angular Dropdown dark - - -

- Opt into darker dropdowns to match a dark navbar or custom style by set - dark property. No changes are required to the dropdown items. -

- - - -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • -
    -
  • -
  • - -
  • -
-
-
-

And putting it to use in a navbar:

- - - - - Navbar - - -
- - - Home - - - Link - - - - - Dropdown - -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • -
    -
  • -
  • - -
  • -
-
-
- - Disabled - -
-
- - -
-
-
-
-
-
-
-
- - - - Angular Dropdown Dropup - - -

- Trigger dropdown menus above elements by adding - direction="dropup" to the <c-dropdown> - component. -

- - - -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • -
    -
  • -
  • - -
  • -
-
- - - -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • -
    -
  • -
  • - -
  • -
-
-
-
-
-
- - - - Angular Dropdown Dropright - - -

- Trigger dropdown menus at the right of the elements by adding - direction="dropend" to the <c-dropdown> - component. -

- - - -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • -
    -
  • -
  • - -
  • -
-
- - - -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • -
    -
  • -
  • - -
  • -
-
-
-
-
-
- - - - Angular Dropdown Dropleft - - -

- Trigger dropdown menus at the left of the elements by adding - direction="dropstart" to the <c-dropdown> - component. -

- - - - -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • -
    -
  • -
  • - -
  • -
- -
-
-
-
-
-
- - - - Angular Dropdown Centered - - -

- Trigger dropdown menus centered below the toggle by adding direction="center" to the c-dropdown - component. -

- - - -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
- - -
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
-
-
-
-
-
diff --git a/src/app/views/buttons/dropdowns/dropdowns.component.spec.ts b/src/app/views/buttons/dropdowns/dropdowns.component.spec.ts deleted file mode 100644 index 8dc7a467d..000000000 --- a/src/app/views/buttons/dropdowns/dropdowns.component.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { - ButtonGroupModule, - ButtonModule, - CardModule, - CollapseModule, - DropdownModule, - GridModule, - NavbarModule, - NavModule -} from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { DropdownsComponent } from './dropdowns.component'; - -describe('DropdownsComponent', () => { - let component: DropdownsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ButtonModule, DropdownModule, CollapseModule, NoopAnimationsModule, GridModule, CardModule, RouterTestingModule, NavModule, NavbarModule, ButtonGroupModule, DropdownsComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(DropdownsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/buttons/dropdowns/dropdowns.component.ts b/src/app/views/buttons/dropdowns/dropdowns.component.ts deleted file mode 100644 index d4a975876..000000000 --- a/src/app/views/buttons/dropdowns/dropdowns.component.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Component } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; -import { RouterLink } from '@angular/router'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { - ButtonDirective, - ButtonGroupComponent, - CardBodyComponent, - CardComponent, - CardHeaderComponent, - ColComponent, - CollapseDirective, - ContainerComponent, - DropdownComponent, - DropdownDividerDirective, - DropdownHeaderDirective, - DropdownItemDirective, - DropdownMenuDirective, - DropdownToggleDirective, - FormControlDirective, - FormDirective, - NavbarBrandDirective, - NavbarComponent, - NavbarNavComponent, - NavbarTogglerDirective, - NavItemComponent, - NavLinkDirective, - RowComponent, - TextColorDirective, - ThemeDirective -} from '@coreui/angular'; - -@Component({ - selector: 'app-dropdowns', - templateUrl: './dropdowns.component.html', - standalone: true, - imports: [ - RowComponent, - ColComponent, - TextColorDirective, - CardComponent, - CardHeaderComponent, - CardBodyComponent, - DocsExampleComponent, - ThemeDirective, - DropdownComponent, - ButtonDirective, - DropdownToggleDirective, - DropdownMenuDirective, - DropdownHeaderDirective, - DropdownItemDirective, - RouterLink, - DropdownDividerDirective, - NavbarComponent, - ContainerComponent, - NavbarBrandDirective, - NavbarTogglerDirective, - CollapseDirective, - NavbarNavComponent, - NavItemComponent, - NavLinkDirective, - ReactiveFormsModule, - FormDirective, - FormControlDirective, - ButtonGroupComponent - ] -}) -export class DropdownsComponent { - - public colors = ['primary', 'secondary', 'success', 'info', 'warning', 'danger']; - - constructor() { } - -} diff --git a/src/app/views/buttons/routes.ts b/src/app/views/buttons/routes.ts deleted file mode 100644 index 446f67e5d..000000000 --- a/src/app/views/buttons/routes.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Routes } from '@angular/router'; - -export const routes: Routes = [ - { - path: '', - data: { - title: 'Buttons' - }, - children: [ - { - path: '', - redirectTo: 'buttons', - pathMatch: 'full' - }, - { - path: 'buttons', - loadComponent: () => import('./buttons/buttons.component').then(m => m.ButtonsComponent), - data: { - title: 'Buttons' - } - }, - { - path: 'button-groups', - loadComponent: () => import('./button-groups/button-groups.component').then(m => m.ButtonGroupsComponent), - data: { - title: 'Button groups' - } - }, - { - path: 'dropdowns', - loadComponent: () => import('./dropdowns/dropdowns.component').then(m => m.DropdownsComponent), - data: { - title: 'Dropdowns' - } - }, - ] - } -]; - diff --git a/src/app/views/charts/charts.component.html b/src/app/views/charts/charts.component.html deleted file mode 100644 index 6e647c5a3..000000000 --- a/src/app/views/charts/charts.component.html +++ /dev/null @@ -1,72 +0,0 @@ - - - - CoreUI Angular wrapper component for Chart.js 4.4, the most popular charting library. -
-
-
- - - - Bar Chart - - - - - - - - - - Line Chart - - - - - - -
- - - - - Doughnut Chart - - - - - - - - - - Pie Chart - - - - - - - - - - - - Polar Area Chart - - - - - - - - - - Radar Chart - - - - - - - diff --git a/src/app/views/charts/charts.component.scss b/src/app/views/charts/charts.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/charts/charts.component.spec.ts b/src/app/views/charts/charts.component.spec.ts deleted file mode 100644 index f7a3a2319..000000000 --- a/src/app/views/charts/charts.component.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; - -import { CardModule, GridModule } from '@coreui/angular'; -import { ChartjsModule } from '@coreui/angular-chartjs'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../icons/icon-subset'; -import { ChartsComponent } from './charts.component'; - -describe('ChartsComponent', () => { - let component: ChartsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [GridModule, CardModule, ChartjsModule, ChartsComponent], - providers: [IconSetService] -}).compileComponents(); - })); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(ChartsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/charts/charts.component.ts b/src/app/views/charts/charts.component.ts deleted file mode 100644 index 0e0f9cf8f..000000000 --- a/src/app/views/charts/charts.component.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Component } from '@angular/core'; -import { ChartData } from 'chart.js'; -import { ChartjsComponent } from '@coreui/angular-chartjs'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent } from '@coreui/angular'; -import { DocsCalloutComponent } from '@docs-components/public-api'; - -@Component({ - selector: 'app-charts', - templateUrl: './charts.component.html', - styleUrls: ['./charts.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, DocsCalloutComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, ChartjsComponent] -}) -export class ChartsComponent { - - options = { - maintainAspectRatio: false - }; - - months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; - - chartBarData: ChartData = { - labels: [...this.months].slice(0, 7), - datasets: [ - { - label: 'GitHub Commits', - backgroundColor: '#f87979', - data: [40, 20, 12, 39, 17, 42, 79] - } - ] - }; - - // chartBarOptions = { - // maintainAspectRatio: false, - // }; - - chartLineData: ChartData = { - labels: [...this.months].slice(0, 7), - datasets: [ - { - label: 'My First dataset', - backgroundColor: 'rgba(220, 220, 220, 0.2)', - borderColor: 'rgba(220, 220, 220, 1)', - pointBackgroundColor: 'rgba(220, 220, 220, 1)', - pointBorderColor: '#fff', - data: [this.randomData, this.randomData, this.randomData, this.randomData, this.randomData, this.randomData, this.randomData] - }, - { - label: 'My Second dataset', - backgroundColor: 'rgba(151, 187, 205, 0.2)', - borderColor: 'rgba(151, 187, 205, 1)', - pointBackgroundColor: 'rgba(151, 187, 205, 1)', - pointBorderColor: '#fff', - data: [this.randomData, this.randomData, this.randomData, this.randomData, this.randomData, this.randomData, this.randomData] - } - ] - }; - - chartLineOptions = { - maintainAspectRatio: false - }; - - chartDoughnutData: ChartData = { - labels: ['VueJs', 'EmberJs', 'ReactJs', 'Angular'], - datasets: [ - { - backgroundColor: ['#41B883', '#E46651', '#00D8FF', '#DD1B16'], - data: [40, 20, 80, 10] - } - ] - }; - - // chartDoughnutOptions = { - // aspectRatio: 1, - // responsive: true, - // maintainAspectRatio: false, - // radius: '100%' - // }; - - chartPieData: ChartData = { - labels: ['Red', 'Green', 'Yellow'], - datasets: [ - { - data: [300, 50, 100], - backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56'], - hoverBackgroundColor: ['#FF6384', '#36A2EB', '#FFCE56'] - } - ] - }; - - // chartPieOptions = { - // aspectRatio: 1, - // responsive: true, - // maintainAspectRatio: false, - // radius: '100%' - // }; - - chartPolarAreaData: ChartData = { - labels: ['Red', 'Green', 'Yellow', 'Grey', 'Blue'], - datasets: [ - { - data: [11, 16, 7, 3, 14], - backgroundColor: ['#FF6384', '#4BC0C0', '#FFCE56', '#E7E9ED', '#36A2EB'] - } - ] - }; - - chartRadarData: ChartData = { - labels: ['Eating', 'Drinking', 'Sleeping', 'Designing', 'Coding', 'Cycling', 'Running'], - datasets: [ - { - label: '2020', - backgroundColor: 'rgba(179,181,198,0.2)', - borderColor: 'rgba(179,181,198,1)', - pointBackgroundColor: 'rgba(179,181,198,1)', - pointBorderColor: '#fff', - pointHoverBackgroundColor: '#fff', - pointHoverBorderColor: 'rgba(179,181,198,1)', - data: [65, 59, 90, 81, 56, 55, 40] - }, - { - label: '2021', - backgroundColor: 'rgba(255,99,132,0.2)', - borderColor: 'rgba(255,99,132,1)', - pointBackgroundColor: 'rgba(255,99,132,1)', - pointBorderColor: '#fff', - pointHoverBackgroundColor: '#fff', - pointHoverBorderColor: 'rgba(255,99,132,1)', - data: [this.randomData, this.randomData, this.randomData, this.randomData, this.randomData, this.randomData, this.randomData] - } - ] - }; - - // chartRadarOptions = { - // aspectRatio: 1.5, - // responsive: true, - // maintainAspectRatio: false, - // }; - - get randomData() { - return Math.round(Math.random() * 100); - } - -} diff --git a/src/app/views/dashboard/dashboard-charts-data.ts b/src/app/views/dashboard/dashboard-charts-data.ts deleted file mode 100644 index c4c862b41..000000000 --- a/src/app/views/dashboard/dashboard-charts-data.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { Injectable } from '@angular/core'; -import { - ChartData, - ChartDataset, - ChartOptions, - ChartType, - PluginOptionsByType, - ScaleOptions, - TooltipLabelStyle -} from 'chart.js'; -import { DeepPartial } from 'chart.js/dist/types/utils'; -import { getStyle, hexToRgba } from '@coreui/utils'; - -export interface IChartProps { - data?: ChartData; - labels?: any; - options?: ChartOptions; - colors?: any; - type: ChartType; - legend?: any; - - [propName: string]: any; -} - -@Injectable({ - providedIn: 'any' -}) -export class DashboardChartsData { - constructor() { - this.initMainChart(); - } - - public mainChart: IChartProps = { type: 'line' }; - - public random(min: number, max: number) { - return Math.floor(Math.random() * (max - min + 1) + min); - } - - initMainChart(period: string = 'Month') { - const brandSuccess = getStyle('--cui-success') ?? '#4dbd74'; - const brandInfo = getStyle('--cui-info') ?? '#20a8d8'; - const brandInfoBg = hexToRgba(getStyle('--cui-info') ?? '#20a8d8', 10); - const brandDanger = getStyle('--cui-danger') ?? '#f86c6b'; - - // mainChart - this.mainChart['elements'] = period === 'Month' ? 12 : 27; - this.mainChart['Data1'] = []; - this.mainChart['Data2'] = []; - this.mainChart['Data3'] = []; - - // generate random values for mainChart - for (let i = 0; i <= this.mainChart['elements']; i++) { - this.mainChart['Data1'].push(this.random(50, 240)); - this.mainChart['Data2'].push(this.random(20, 160)); - this.mainChart['Data3'].push(65); - } - - let labels: string[] = []; - if (period === 'Month') { - labels = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December' - ]; - } else { - /* tslint:disable:max-line-length */ - const week = [ - 'Monday', - 'Tuesday', - 'Wednesday', - 'Thursday', - 'Friday', - 'Saturday', - 'Sunday' - ]; - labels = week.concat(week, week, week); - } - - const colors = [ - { - // brandInfo - backgroundColor: brandInfoBg, - borderColor: brandInfo, - pointHoverBackgroundColor: brandInfo, - borderWidth: 2, - fill: true - }, - { - // brandSuccess - backgroundColor: 'transparent', - borderColor: brandSuccess || '#4dbd74', - pointHoverBackgroundColor: '#fff' - }, - { - // brandDanger - backgroundColor: 'transparent', - borderColor: brandDanger || '#f86c6b', - pointHoverBackgroundColor: brandDanger, - borderWidth: 1, - borderDash: [8, 5] - } - ]; - - const datasets: ChartDataset[] = [ - { - data: this.mainChart['Data1'], - label: 'Current', - ...colors[0] - }, - { - data: this.mainChart['Data2'], - label: 'Previous', - ...colors[1] - }, - { - data: this.mainChart['Data3'], - label: 'BEP', - ...colors[2] - } - ]; - - const plugins: DeepPartial> = { - legend: { - display: false - }, - tooltip: { - callbacks: { - labelColor: (context) => ({ backgroundColor: context.dataset.borderColor } as TooltipLabelStyle) - } - } - }; - - const scales = this.getScales(); - - const options: ChartOptions = { - maintainAspectRatio: false, - plugins, - scales, - elements: { - line: { - tension: 0.4 - }, - point: { - radius: 0, - hitRadius: 10, - hoverRadius: 4, - hoverBorderWidth: 3 - } - } - }; - - this.mainChart.type = 'line'; - this.mainChart.options = options; - this.mainChart.data = { - datasets, - labels - }; - } - - getScales() { - const colorBorderTranslucent = getStyle('--cui-border-color-translucent'); - const colorBody = getStyle('--cui-body-color'); - - const scales: ScaleOptions = { - x: { - grid: { - color: colorBorderTranslucent, - drawOnChartArea: false - }, - ticks: { - color: colorBody - } - }, - y: { - border: { - color: colorBorderTranslucent - }, - grid: { - color: colorBorderTranslucent - }, - max: 250, - beginAtZero: true, - ticks: { - color: colorBody, - maxTicksLimit: 5, - stepSize: Math.ceil(250 / 5) - } - } - }; - return scales; - } -} diff --git a/src/app/views/dashboard/dashboard.component.html b/src/app/views/dashboard/dashboard.component.html deleted file mode 100644 index 98a251739..000000000 --- a/src/app/views/dashboard/dashboard.component.html +++ /dev/null @@ -1,395 +0,0 @@ - - - - - -

Traffic

-
January - December 2023
-
- - -
- - - - - - - - -
-
-
- - Main chart - -
- - - -
Visits
- 29.703 Users (40%) - -
- -
Unique
-
24.093 Users (20%)
- -
- -
Page views
-
78.706 Views (60%)
- -
- -
New Users
-
22.123 Users (80%)
- -
- -
Bounce Rate
-
Average Rate (40.15%)
- -
-
-
-
- - - - - - - Traffic {{ "&" }} Sales - - - - - -
-
New Clients
-
9,123
-
-
- -
-
- Recurring Clients -
-
22,643
-
-
-
-
-
-
- Monday -
-
- - -
-
-
-
- Tuesday -
-
- - -
-
-
-
- Wednesday -
-
- - -
-
-
-
- Thursday -
-
- - -
-
-
-
- Friday -
-
- - -
-
-
-
- Saturday -
-
- - -
-
-
-
- Sunday -
-
- - -
-
-
- - -   - - New clients -    - -   - - Recurring clients - -
-
- - - - -
-
Page views
-
78,623
-
-
- -
-
Organic
-
49,123
-
-
-
- -
- -
-
- - Male - 43% -
-
- -
-
-
-
- - Female - 37% -
-
- -
-
- -
-
- - Organic Search - - 191,235 (56%) - -
-
- -
-
-
-
- - Facebook - - 51,223 (15%) - -
-
- -
-
-
-
- - Twitter - - 37,564 (11%) - -
-
- -
-
-
-
- - LinkedIn - - 27,319 (8%) - -
-
- -
-
-
- -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - @for (user of users; track user.name; let i = $index) { - - - - - - - - - } - -
- - UserCountryUsagePayment MethodActivity
- - -
{{ user.name }}
-
- - {{ user.state }} | Registered: {{ user.registered }} - -
-
- - -
-
- {{ user.usage }}% -
-
- - {{ user.period }} - -
-
- -
- - -
Last login
-
{{ user.activity }}
-
-
-
-
-
diff --git a/src/app/views/dashboard/dashboard.component.scss b/src/app/views/dashboard/dashboard.component.scss deleted file mode 100644 index 3e08d9395..000000000 --- a/src/app/views/dashboard/dashboard.component.scss +++ /dev/null @@ -1,7 +0,0 @@ -:host { - .legend { - small { - font-size: x-small; - } - } -} diff --git a/src/app/views/dashboard/dashboard.component.ts b/src/app/views/dashboard/dashboard.component.ts deleted file mode 100644 index 4be7181e2..000000000 --- a/src/app/views/dashboard/dashboard.component.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { DOCUMENT, NgStyle } from '@angular/common'; -import { Component, DestroyRef, effect, inject, OnInit, Renderer2, signal, WritableSignal } from '@angular/core'; -import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; -import { ChartOptions } from 'chart.js'; -import { - AvatarComponent, - ButtonDirective, - ButtonGroupComponent, - CardBodyComponent, - CardComponent, - CardFooterComponent, - CardHeaderComponent, - ColComponent, - FormCheckLabelDirective, - GutterDirective, - ProgressBarDirective, - ProgressComponent, - RowComponent, - TableDirective, - TextColorDirective -} from '@coreui/angular'; -import { ChartjsComponent } from '@coreui/angular-chartjs'; -import { IconDirective } from '@coreui/icons-angular'; - -import { WidgetsBrandComponent } from '../widgets/widgets-brand/widgets-brand.component'; -import { WidgetsDropdownComponent } from '../widgets/widgets-dropdown/widgets-dropdown.component'; -import { DashboardChartsData, IChartProps } from './dashboard-charts-data'; - -interface IUser { - name: string; - state: string; - registered: string; - country: string; - usage: number; - period: string; - payment: string; - activity: string; - avatar: string; - status: string; - color: string; -} - -@Component({ - templateUrl: 'dashboard.component.html', - styleUrls: ['dashboard.component.scss'], - standalone: true, - imports: [WidgetsDropdownComponent, TextColorDirective, CardComponent, CardBodyComponent, RowComponent, ColComponent, ButtonDirective, IconDirective, ReactiveFormsModule, ButtonGroupComponent, FormCheckLabelDirective, ChartjsComponent, NgStyle, CardFooterComponent, GutterDirective, ProgressBarDirective, ProgressComponent, WidgetsBrandComponent, CardHeaderComponent, TableDirective, AvatarComponent] -}) -export class DashboardComponent implements OnInit { - - readonly #destroyRef: DestroyRef = inject(DestroyRef); - readonly #document: Document = inject(DOCUMENT); - readonly #renderer: Renderer2 = inject(Renderer2); - readonly #chartsData: DashboardChartsData = inject(DashboardChartsData); - - public users: IUser[] = [ - { - name: 'Yiorgos Avraamu', - state: 'New', - registered: 'Jan 1, 2021', - country: 'Us', - usage: 50, - period: 'Jun 11, 2021 - Jul 10, 2021', - payment: 'Mastercard', - activity: '10 sec ago', - avatar: './assets/images/avatars/1.jpg', - status: 'success', - color: 'success' - }, - { - name: 'Avram Tarasios', - state: 'Recurring ', - registered: 'Jan 1, 2021', - country: 'Br', - usage: 10, - period: 'Jun 11, 2021 - Jul 10, 2021', - payment: 'Visa', - activity: '5 minutes ago', - avatar: './assets/images/avatars/2.jpg', - status: 'danger', - color: 'info' - }, - { - name: 'Quintin Ed', - state: 'New', - registered: 'Jan 1, 2021', - country: 'In', - usage: 74, - period: 'Jun 11, 2021 - Jul 10, 2021', - payment: 'Stripe', - activity: '1 hour ago', - avatar: './assets/images/avatars/3.jpg', - status: 'warning', - color: 'warning' - }, - { - name: 'Enéas Kwadwo', - state: 'Sleep', - registered: 'Jan 1, 2021', - country: 'Fr', - usage: 98, - period: 'Jun 11, 2021 - Jul 10, 2021', - payment: 'Paypal', - activity: 'Last month', - avatar: './assets/images/avatars/4.jpg', - status: 'secondary', - color: 'danger' - }, - { - name: 'Agapetus Tadeáš', - state: 'New', - registered: 'Jan 1, 2021', - country: 'Es', - usage: 22, - period: 'Jun 11, 2021 - Jul 10, 2021', - payment: 'ApplePay', - activity: 'Last week', - avatar: './assets/images/avatars/5.jpg', - status: 'success', - color: 'primary' - }, - { - name: 'Friderik Dávid', - state: 'New', - registered: 'Jan 1, 2021', - country: 'Pl', - usage: 43, - period: 'Jun 11, 2021 - Jul 10, 2021', - payment: 'Amex', - activity: 'Yesterday', - avatar: './assets/images/avatars/6.jpg', - status: 'info', - color: 'dark' - } - ]; - - public mainChart: IChartProps = { type: 'line' }; - public mainChartRef: WritableSignal = signal(undefined); - #mainChartRefEffect = effect(() => { - if (this.mainChartRef()) { - this.setChartStyles(); - } - }); - public chart: Array = []; - public trafficRadioGroup = new FormGroup({ - trafficRadio: new FormControl('Month') - }); - - ngOnInit(): void { - this.initCharts(); - this.updateChartOnColorModeChange(); - } - - initCharts(): void { - this.mainChart = this.#chartsData.mainChart; - } - - setTrafficPeriod(value: string): void { - this.trafficRadioGroup.setValue({ trafficRadio: value }); - this.#chartsData.initMainChart(value); - this.initCharts(); - } - - handleChartRef($chartRef: any) { - if ($chartRef) { - this.mainChartRef.set($chartRef); - } - } - - updateChartOnColorModeChange() { - const unListen = this.#renderer.listen(this.#document.documentElement, 'ColorSchemeChange', () => { - this.setChartStyles(); - }); - - this.#destroyRef.onDestroy(() => { - unListen(); - }); - } - - setChartStyles() { - if (this.mainChartRef()) { - setTimeout(() => { - const options: ChartOptions = { ...this.mainChart.options }; - const scales = this.#chartsData.getScales(); - this.mainChartRef().options.scales = { ...options.scales, ...scales }; - this.mainChartRef().update(); - }); - } - } -} diff --git a/src/app/views/forms/checks-radios/checks-radios.component.html b/src/app/views/forms/checks-radios/checks-radios.component.html deleted file mode 100644 index 36ccbb4db..000000000 --- a/src/app/views/forms/checks-radios/checks-radios.component.html +++ /dev/null @@ -1,335 +0,0 @@ - -
- - - - Angular Checkbox - - - -
- - - - - - - - -
-
-
-
-
- - - - Angular Checkbox Disabled - - -

- Add the disabled attribute and the associated <label>s - are automatically styled to match with a lighter color to help indicate the - input's state. -

- -
- - - - - - - - -
-
-
-
-
- - - - Angular Radio - - - -
- - - - - - - - -
-
-
-
-
- - - - Angular Radio Disabled - - -

- Add the disabled attribute and the associated <label>s - are automatically styled to match with a lighter color to help indicate the - input's state. -

- -
- - - - - - - - -
-
-
-
-
- - - - Angular Switches - - -

- A switch has the markup of a custom checkbox but uses the switch boolean - properly to render a toggle switch. Switches also support the disabled - attribute. -

- - - - - - - - - - - - - - - - - - -
-
-
- - - - Angular Switches Sizes - - - - - - - - - - - - - - - - - - - - - - - Angular Checks and Radios Default layout (stacked) - - -

- By default, any number of checkboxes and radios that are immediate sibling will be - vertically stacked and appropriately spaced. -

- - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- - - - Angular Checks and Radios Inline - - -

- Group checkboxes or radios on the same horizontal row by adding inline - boolean property to any <c-form-check>. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- - - - Angular Checks and Radios Without labels - - -

- Remember to still provide some form of accessible name for assistive technologies (for - instance, using aria-label). -

- -
- -
-
- -
-
-
-
-
- - - - Toggle buttons - - -

- Create button-like checkboxes and radio buttons by using button boolean - property on the <CFormCheck> component. These toggle buttons can - further be grouped in a button group if needed. -

- -
- - - - - - - - - - -
-
- -
- - - - - - - - - -
-
-
Outlined styles
-

- Different variants of button, such at the various outlined styles, are supported. -

- -
- - - - - - - - - - -
-
- -
- - - - - - - - - - -
-
-
-
-
-
-
diff --git a/src/app/views/forms/checks-radios/checks-radios.component.scss b/src/app/views/forms/checks-radios/checks-radios.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/forms/checks-radios/checks-radios.component.spec.ts b/src/app/views/forms/checks-radios/checks-radios.component.spec.ts deleted file mode 100644 index aeda34321..000000000 --- a/src/app/views/forms/checks-radios/checks-radios.component.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { ButtonGroupModule, ButtonModule, CardModule, FormModule, GridModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { ChecksRadiosComponent } from './checks-radios.component'; - -describe('ChecksRadiosComponent', () => { - let component: ChecksRadiosComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CardModule, GridModule, ButtonModule, FormModule, ReactiveFormsModule, RouterTestingModule, ButtonGroupModule, ChecksRadiosComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(ChecksRadiosComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/forms/checks-radios/checks-radios.component.ts b/src/app/views/forms/checks-radios/checks-radios.component.ts deleted file mode 100644 index 508a98e90..000000000 --- a/src/app/views/forms/checks-radios/checks-radios.component.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Component } from '@angular/core'; -import { FormControl, UntypedFormBuilder, ReactiveFormsModule } from '@angular/forms'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, FormDirective, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, FormCheckComponent, FormCheckInputDirective, FormCheckLabelDirective, ButtonGroupComponent, ButtonDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-checks-radios', - templateUrl: './checks-radios.component.html', - styleUrls: ['./checks-radios.component.scss'], - standalone: true, - imports: [RowComponent, ReactiveFormsModule, FormDirective, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, FormCheckComponent, FormCheckInputDirective, FormCheckLabelDirective, ButtonGroupComponent, ButtonDirective] -}) -export class ChecksRadiosComponent { - -inputDisabled: null = null; - - formGroup = this.formBuilder.group({ - flexRadioGroup: this.formBuilder.group({ - flexRadioDefault: this.formBuilder.control('two') - }), - flexRadioGroupDisabled: this.formBuilder.group({ - flexRadioDefault: this.formBuilder.control({ value: 'two', disabled: true }) - }), - flexCheckGroup: this.formBuilder.group({ - checkOne: [false], - checkTwo: [true] - }), - flexCheckGroupDisabled: this.formBuilder.group({ - checkThree: [{ value: false, disabled: true }], - checkFour: [{ value: true, disabled: true }] - }), - btnCheckGroup: this.formBuilder.group({ - checkbox1: [true], - checkbox2: [false], - checkbox3: [{value: false, disabled: true}] - }), - btnRadioGroup: this.formBuilder.group({ - radio1: this.formBuilder.control({ value: 'Radio2' }) - }) - }); - - - constructor( - private formBuilder: UntypedFormBuilder - ) { } - - setCheckBoxValue(controlName: string) { - const btnCheckGroup = this.formGroup.controls['btnCheckGroup']; - const prevValue = btnCheckGroup.get(controlName)?.value; - const groupValue = {...btnCheckGroup.value}; - groupValue[controlName] = !prevValue; - btnCheckGroup.patchValue(groupValue); - } - - setRadioValue(value: string): void { - const group = this.formGroup.controls['btnRadioGroup']; - group.setValue({ radio1: value }); - } - -} diff --git a/src/app/views/forms/floating-labels/floating-labels.component.html b/src/app/views/forms/floating-labels/floating-labels.component.html deleted file mode 100644 index c374c6bdf..000000000 --- a/src/app/views/forms/floating-labels/floating-labels.component.html +++ /dev/null @@ -1,154 +0,0 @@ - - - - - Angular Floating labels - - -

- Wrap a pair of <cFormControl> and <label> - elements in cFormControl to enable floating labels with textual form - fields. A placeholder is required on each <input> - as our method of CSS-only floating labels uses the :placeholder-shown - pseudo-element. Also note that the <cFormControl> must come first so - we can utilize a sibling selector (e.g., ~). -

- -
- - -
-
- - -
-
-

- When there's a value already defined, <label> - s will automatically adjust to their floated position. -

- -
- - -
-
-
-
-
- - - - Angular Floating labels Textareas - - -

- By default, <CFormTextarea>s will be the same height as - <input>s. -

- -
- - -
-
-

- To set a custom height on your <CFormTextarea;>, do not use the - rows attribute. Instead, set an explicit height (either - inline or via custom CSS). -

- -
- - -
-
-
-
-
- - - - Angular Floating labels Selects - - -

- Other than <input>, floating labels are only available on - <cSelect>s. They work in the same way, but unlike - <input>s, they'll always show the - <label> in its floated state. - - Selects with size and multiple are not supported. - -

- -
- - -
-
-
-
-
- - - - Angular Floating labels Layout - - -

- When working with the CoreUI for Bootstrap grid system, be sure to place form elements - within column classes. -

- - - -
- - -
-
- -
- - -
-
-
-
-
-
-
-
- diff --git a/src/app/views/forms/floating-labels/floating-labels.component.scss b/src/app/views/forms/floating-labels/floating-labels.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/forms/floating-labels/floating-labels.component.spec.ts b/src/app/views/forms/floating-labels/floating-labels.component.spec.ts deleted file mode 100644 index eb9a1aa58..000000000 --- a/src/app/views/forms/floating-labels/floating-labels.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { CardModule, FormModule, GridModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { FloatingLabelsComponent } from './floating-labels.component'; - -describe('FloatingLabelsComponent', () => { - let component: FloatingLabelsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CardModule, GridModule, RouterTestingModule, FormModule, FloatingLabelsComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(FloatingLabelsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/forms/floating-labels/floating-labels.component.ts b/src/app/views/forms/floating-labels/floating-labels.component.ts deleted file mode 100644 index 904def44c..000000000 --- a/src/app/views/forms/floating-labels/floating-labels.component.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Component } from '@angular/core'; -import { NgStyle } from '@angular/common'; -import { ReactiveFormsModule, FormsModule } from '@angular/forms'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, FormFloatingDirective, FormControlDirective, FormLabelDirective, FormDirective, FormSelectDirective, GutterDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-floating-labels', - templateUrl: './floating-labels.component.html', - styleUrls: ['./floating-labels.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, FormFloatingDirective, FormControlDirective, FormLabelDirective, ReactiveFormsModule, FormsModule, FormDirective, NgStyle, FormSelectDirective, GutterDirective] -}) -export class FloatingLabelsComponent { - - constructor() { } - -} diff --git a/src/app/views/forms/form-controls/form-controls.component.html b/src/app/views/forms/form-controls/form-controls.component.html deleted file mode 100644 index 9a4409fa1..000000000 --- a/src/app/views/forms/form-controls/form-controls.component.html +++ /dev/null @@ -1,238 +0,0 @@ - - - - - Angular Form Control - - - -
-
- - -
-
- - -
-
-
-
-
-
- - - - Angular Form Control Sizing - - -

- Set heights using sizing property like sizing="lg" and - sizing="sm". -

- - -
- -
- -
-
-
-
- - - - Angular Form Control Disabled - - -

- Add the disabled boolean attribute on an input to give it a grayed out - appearance and remove pointer events. -

- - -
- -
-
-
-
-
- - - - Angular Form Control Readonly - - -

- Add the readOnly boolean attribute on an input to prevent modification of - the input's value. Read-only inputs appear lighter (just like disabled inputs), - but retain the standard cursor. -

- - - -
-
-
- - - - Angular Form Control Readonly plain text - - -

- If you want to have <input readonly> elements in your form styled - as plain text, use the plainText boolean property to remove the default - form field styling and preserve the correct margin and padding. -

- - - -
- -
-
- - -
- -
-
-
- -
-
- - -
-
- - -
-
- -
-
-
-
-
-
- - - - Angular Form Control File input - - - -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
- - - - Angular Form Control Color - - - - - - - - - - - -
-
- - {{favoriteColor}} - -
-
-
-
-
- diff --git a/src/app/views/forms/form-controls/form-controls.component.scss b/src/app/views/forms/form-controls/form-controls.component.scss deleted file mode 100644 index f14fad017..000000000 --- a/src/app/views/forms/form-controls/form-controls.component.scss +++ /dev/null @@ -1,9 +0,0 @@ -:host { - #exampleColorInput { - min-width: 2.5rem; - } - .color-box { - min-width: 2rem; - min-height: 2rem; - } -} diff --git a/src/app/views/forms/form-controls/form-controls.component.spec.ts b/src/app/views/forms/form-controls/form-controls.component.spec.ts deleted file mode 100644 index 1f0524c3c..000000000 --- a/src/app/views/forms/form-controls/form-controls.component.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { ButtonModule, CardModule, FormModule, GridModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { FormControlsComponent } from './form-controls.component'; - -describe('FormControlsComponent', () => { - let component: FormControlsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CardModule, GridModule, FormsModule, FormModule, ButtonModule, RouterTestingModule, FormControlsComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(FormControlsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/forms/form-controls/form-controls.component.ts b/src/app/views/forms/form-controls/form-controls.component.ts deleted file mode 100644 index a198b4392..000000000 --- a/src/app/views/forms/form-controls/form-controls.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Component } from '@angular/core'; -import { NgStyle } from '@angular/common'; -import { ReactiveFormsModule, FormsModule } from '@angular/forms'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, FormDirective, FormLabelDirective, FormControlDirective, ButtonDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-form-controls', - templateUrl: './form-controls.component.html', - styleUrls: ['./form-controls.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, ReactiveFormsModule, FormsModule, FormDirective, FormLabelDirective, FormControlDirective, ButtonDirective, NgStyle] -}) -export class FormControlsComponent { - - public favoriteColor = '#26ab3c'; - - constructor() { } - -} diff --git a/src/app/views/forms/input-groups/input-groups.component.html b/src/app/views/forms/input-groups/input-groups.component.html deleted file mode 100644 index 75cb4bb88..000000000 --- a/src/app/views/forms/input-groups/input-groups.component.html +++ /dev/null @@ -1,522 +0,0 @@ - - - - - Angular Input group Basic example - - -

- Place one add-on or button on either side of an input. You may also place one on both - sides of an input. Remember to place <label>s outside the - input group. -

- - - @ - - - - - @example.com - - - - https://example.com/users/ - - - - $ - - .00 - - - - @ - - - - With textarea - - - -
-
-
- - - - Angular Input group Wrapping - - -

- Input groups wrap by default via flex-wrap: wrap in order to accommodate - custom form field validation within an input group. You may disable this with - .flex-nowrap. -

- - - @ - - - -
-
-
- - - - Angular Input group Sizing - - -

- Add the relative form sizing classes to the <c-input-group> itself - and contents within will automatically resize—no need for repeating the form control - size classes on each element. -

-

- Sizing on the individual input group elements isn't supported. -

- - - Small - - - - Default - - - - Large - - - -
-
-
- - - - Angular Input group Checkboxes and radios - - -

- Place any checkbox or radio option within an input group's addon instead of text. -

- - - -
- -
-
- -
- - -
- -
-
- -
-
-
-
-
- - - - Angular Input group Multiple inputs - - -

- While multiple <CFormInput>s are supported visually, validation - styles are only available for input groups with a single - cFormControl. -

- - - First and last name - - - - -
-
-
- - - - Angular Input group Multiple addons - - -

- Multiple add-ons are supported and can be mixed with checkbox and radio input - versions.. -

- - - $ - 0.00 - - - - - $ - 0.00 - - -
-
-
- - - - Angular Input group Button addons - - -

- Button add-ons are supported. -

- - - - - - - - - - - - - - - - - - - - -
-
-
- - - - Angular Input group Buttons with dropdowns - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Angular Input group Segmented buttons - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Angular Input group Custom select - - - - - - - - - - - - - - - - - - - - - - - - - - - Angular Input group Custom file input - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/app/views/forms/input-groups/input-groups.component.scss b/src/app/views/forms/input-groups/input-groups.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/forms/input-groups/input-groups.component.spec.ts b/src/app/views/forms/input-groups/input-groups.component.spec.ts deleted file mode 100644 index b8c81322a..000000000 --- a/src/app/views/forms/input-groups/input-groups.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { ButtonModule, CardModule, DropdownModule, FormModule, GridModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { InputGroupsComponent } from './input-groups.component'; - -describe('InputGroupsComponent', () => { - let component: InputGroupsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [FormModule, CardModule, GridModule, ButtonModule, DropdownModule, RouterTestingModule, InputGroupsComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(InputGroupsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/forms/input-groups/input-groups.component.ts b/src/app/views/forms/input-groups/input-groups.component.ts deleted file mode 100644 index 9efe76bdf..000000000 --- a/src/app/views/forms/input-groups/input-groups.component.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Component } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; -import { RouterLink } from '@angular/router'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, InputGroupComponent, InputGroupTextDirective, FormControlDirective, FormLabelDirective, FormCheckInputDirective, ButtonDirective, ThemeDirective, DropdownComponent, DropdownToggleDirective, DropdownMenuDirective, DropdownItemDirective, DropdownDividerDirective, FormSelectDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-input-groups', - templateUrl: './input-groups.component.html', - styleUrls: ['./input-groups.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, InputGroupComponent, InputGroupTextDirective, FormControlDirective, FormLabelDirective, FormCheckInputDirective, ButtonDirective, ThemeDirective, DropdownComponent, DropdownToggleDirective, DropdownMenuDirective, DropdownItemDirective, RouterLink, DropdownDividerDirective, FormSelectDirective, ReactiveFormsModule] -}) -export class InputGroupsComponent { - - constructor() { } - -} diff --git a/src/app/views/forms/layout/layout.component.html b/src/app/views/forms/layout/layout.component.html deleted file mode 100644 index a9b1d9981..000000000 --- a/src/app/views/forms/layout/layout.component.html +++ /dev/null @@ -1,388 +0,0 @@ - - - - - Layout Form grid - - -

- More complex forms can be built using our grid classes. Use these for form layouts - that require multiple columns, varied widths, and additional alignment options. -

- - - - - - - - - - -
-
-
- - - - Layout Gutters - - -

- By adding gutter modifier classes - , you can have control over the gutter width in as well the inline as block direction. -

- - - - - - - - - - -

- More complex layouts can also be created with the grid system. -

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - - Layout Horizontal form - - -

- Create horizontal forms with the grid by adding the .row class to form - groups and using the .col-*-* classes to specify the width of your labels - and controls. Be sure to add .col-form-label to your - <label>s as well so they're vertically centered with their - associated form controls. -

-

- At times, you maybe need to use margin or padding utilities to create that perfect - alignment you need. For example, we've removed the padding-top on our - stacked radio inputs label to better align the text baseline. -

- -
- - - - - - - - - - - - -
- Radios - - - - - - - - - - - - - - -
- - - - - - - - - -
-
-
-
-
- - - - Layout Horizontal form label sizing - - -

- Be sure to use .col-form-label-sm or .col-form-label-lg to - your <label>s or <legend>s to correctly - follow the size of .form-control-lg and .form-control-sm. -

- - - - - - - - - - - - - - - - - - - - -
-
-
- - - - Layout Column sizing - - -

- As shown in the previous examples, our grid system allows you to place any number of - <c-col>s within a <c-row>. They'll split the - available width equally between them. You may also pick a subset of your columns to - take up more or less space, while the remaining <c-col>s equally - split the rest, with specific column classes like - <c-col sm="7">. -

- - - - - - - - - - - - - -
-
-
- - - - Layout Auto-sizing - - -

- The example below uses a flexbox utility to vertically center the contents and changes - <c-col> to <c-col xs="auto"> so that your - columns only take up as much space as needed. Put another way, the column sizes itself - based on the contents. -

- -
- - - - - - - - @ - - - - - - - - - - - - - - - - -
-
-

- You can then remix that once again with size-specific column classes. -

- -
- - - - - - - - @ - - - - - - - - - - - - - - - - -
-
-
-
-
- - - - Layout Inline forms - - -

- Use the <c-col xs="auto"> class to create horizontal - layouts. By adding - gutter modifier classes, we will - have gutters in horizontal and vertical directions. The - .align-items-center aligns the form elements to the middle, making the - <CFormCheck> align properly. -

- -
- - - - @ - - - - - - - - - - - - - - - - -
-
-
-
-
-
diff --git a/src/app/views/forms/layout/layout.component.scss b/src/app/views/forms/layout/layout.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/forms/layout/layout.component.spec.ts b/src/app/views/forms/layout/layout.component.spec.ts deleted file mode 100644 index 13ed36344..000000000 --- a/src/app/views/forms/layout/layout.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { ButtonModule, CardModule, FormModule, GridModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { LayoutComponent } from './layout.component'; - -describe('LayoutComponent', () => { - let component: LayoutComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [FormModule, CardModule, GridModule, ButtonModule, RouterTestingModule, LayoutComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(LayoutComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/forms/layout/layout.component.ts b/src/app/views/forms/layout/layout.component.ts deleted file mode 100644 index f169bb091..000000000 --- a/src/app/views/forms/layout/layout.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Component } from '@angular/core'; -import { ReactiveFormsModule, FormsModule } from '@angular/forms'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, FormControlDirective, FormDirective, FormLabelDirective, FormSelectDirective, FormCheckComponent, FormCheckInputDirective, FormCheckLabelDirective, ButtonDirective, ColDirective, InputGroupComponent, InputGroupTextDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-layout', - templateUrl: './layout.component.html', - styleUrls: ['./layout.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, FormControlDirective, ReactiveFormsModule, FormsModule, FormDirective, FormLabelDirective, FormSelectDirective, FormCheckComponent, FormCheckInputDirective, FormCheckLabelDirective, ButtonDirective, ColDirective, InputGroupComponent, InputGroupTextDirective] -}) -export class LayoutComponent { - - constructor() { } - -} diff --git a/src/app/views/forms/ranges/ranges.component.html b/src/app/views/forms/ranges/ranges.component.html deleted file mode 100644 index 40f74fec3..000000000 --- a/src/app/views/forms/ranges/ranges.component.html +++ /dev/null @@ -1,89 +0,0 @@ - - - - - Angular Range - - -

- Create custom <input type="range"> controls - with <input cFormControl type="range">. -

- - - - -
-
-
- - - - Angular Range Disabled - - -

- Add the disabled boolean attribute on an input to give it - a grayed out appearance and remove pointer events. -

- - - - -
-
-
- - - - Angular Range Min and max - - -

- Range inputs have implicit values for min-0 and - max-100, respectively. - You may specify new values for those using the min and - max attributes. -

- - - - -
-
-
- - - - Angular Range Steps - - -

- By default, range inputs "snap" to integer values. To change - this, you can specify a step value. In the example below, - we double the number of steps by using - step="0.5". -

- - - - -
-
-
-
diff --git a/src/app/views/forms/ranges/ranges.component.scss b/src/app/views/forms/ranges/ranges.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/forms/ranges/ranges.component.spec.ts b/src/app/views/forms/ranges/ranges.component.spec.ts deleted file mode 100644 index 2aa527786..000000000 --- a/src/app/views/forms/ranges/ranges.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { CardModule, FormModule, GridModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { RangesComponent } from './ranges.component'; - -describe('RangesComponent', () => { - let component: RangesComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CardModule, GridModule, FormModule, RouterTestingModule, RangesComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(RangesComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/forms/ranges/ranges.component.ts b/src/app/views/forms/ranges/ranges.component.ts deleted file mode 100644 index ff87eaff2..000000000 --- a/src/app/views/forms/ranges/ranges.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component } from '@angular/core'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, FormLabelDirective, FormControlDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-ranges', - templateUrl: './ranges.component.html', - styleUrls: ['./ranges.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, FormLabelDirective, FormControlDirective] -}) -export class RangesComponent { - - constructor() { } - -} diff --git a/src/app/views/forms/routes.ts b/src/app/views/forms/routes.ts deleted file mode 100644 index e413478a1..000000000 --- a/src/app/views/forms/routes.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Routes } from '@angular/router'; - -export const routes: Routes = [ - { - path: '', - data: { - title: 'Forms' - }, - children: [ - { - path: '', - redirectTo: 'form-control', - pathMatch: 'full' - }, - { - path: 'form-control', - loadComponent: () => import('./form-controls/form-controls.component').then(m => m.FormControlsComponent), - data: { - title: 'Form Control' - } - }, - { - path: 'select', - loadComponent: () => import('./select/select.component').then(m => m.SelectComponent), - data: { - title: 'Select' - } - }, - { - path: 'checks-radios', - loadComponent: () => import('./checks-radios/checks-radios.component').then(m => m.ChecksRadiosComponent), - data: { - title: 'Checks & Radios' - } - }, - { - path: 'range', - loadComponent: () => import('./ranges/ranges.component').then(m => m.RangesComponent), - data: { - title: 'Range' - } - }, - { - path: 'input-group', - loadComponent: () => import('./input-groups/input-groups.component').then(m => m.InputGroupsComponent), - data: { - title: 'Input Group' - } - }, - { - path: 'floating-labels', - loadComponent: () => import('./floating-labels/floating-labels.component').then(m => m.FloatingLabelsComponent), - data: { - title: 'Floating Labels' - } - }, - { - path: 'layout', - loadComponent: () => import('./layout/layout.component').then(m => m.LayoutComponent), - data: { - title: 'Layout' - } - }, - { - path: 'validation', - loadComponent: () => import('./validation/validation.component').then(m => m.ValidationComponent), - data: { - title: 'Validation' - } - } - ] - } -]; diff --git a/src/app/views/forms/select/select.component.html b/src/app/views/forms/select/select.component.html deleted file mode 100644 index c5896f23c..000000000 --- a/src/app/views/forms/select/select.component.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - - Angular Select Default - - - - - - - - - - - - Angular Select Sizing - - -

- You may also choose from small and large custom selects to match our similarly sized - text inputs. -

- - - - -

- The multiple attribute is also supported: -

- - - -

- As is the html size property: -

- - - -
-
-
- - - - Angular Select Disabled - - -

- Add the disabled boolean attribute on a select to give it a grayed out - appearance and remove pointer events. -

- - - -
-
-
-
- diff --git a/src/app/views/forms/select/select.component.scss b/src/app/views/forms/select/select.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/forms/select/select.component.spec.ts b/src/app/views/forms/select/select.component.spec.ts deleted file mode 100644 index 0b75b2585..000000000 --- a/src/app/views/forms/select/select.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { CardModule, GridModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { SelectComponent } from './select.component'; - -describe('SelectComponent', () => { - let component: SelectComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CardModule, GridModule, RouterTestingModule, SelectComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(SelectComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/forms/select/select.component.ts b/src/app/views/forms/select/select.component.ts deleted file mode 100644 index 7bea35721..000000000 --- a/src/app/views/forms/select/select.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Component } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, FormSelectDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-select', - templateUrl: './select.component.html', - styleUrls: ['./select.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, FormSelectDirective, ReactiveFormsModule] -}) -export class SelectComponent { - - constructor() { } - -} diff --git a/src/app/views/forms/validation/validation.component.html b/src/app/views/forms/validation/validation.component.html deleted file mode 100644 index 122167b3f..000000000 --- a/src/app/views/forms/validation/validation.component.html +++ /dev/null @@ -1,430 +0,0 @@ - - - - - Validation Custom styles - - -

- For custom CoreUI form validation messages, you'll need to add the - noValidate boolean property to your <CForm>. This - disables the browser default feedback tooltips, but still provides access to the form - validation APIs in JavaScript. Try to submit the form below; our JavaScript will - intercept the submit button and relay feedback to you. When attempting to submit, - you'll see the :invalid and :valid styles applied to - your form controls. -

-

- Custom feedback styles apply custom colors, borders, focus styles, and background - icons to better communicate feedback. -

- - -
- - - - Looks good! - - - - - Looks good! - - - - - @ - - Please choose a username. - - - - - - Please provide a valid city. - - - - - Please provide a valid State. - - - - - Please provide a valid zip. - - - - - - - You must agree before submitting. - - - - - -
-
-
-
-
- - - - Validation Browser defaults - - -

- Not interested in custom validation feedback messages or writing JavaScript to change - form behaviors? All good, you can use the browser defaults. Try submitting the form - below. Depending on your browser and OS, you'll see a slightly different style of - feedback. -

-

- While these feedback styles cannot be styled with CSS, you can still customize the - feedback text through JavaScript. -

- - -
- - - - Looks good! - - - - - Looks good! - - - - - @ - - Please choose a username. - - - - - - Please provide a valid city. - - - - - Please provide a valid State. - - - - - Please provide a valid zip. - - - - - - - You must agree before submitting. - - - - - -
-
-
-
-
- - - - Validation Server side - - -

- We recommend using client-side validation, but in case you require server-side - validation, you can indicate invalid and valid form fields with invalid - and valid boolean properties. -

-

- For invalid fields, ensure that the invalid feedback/error message is associated with - the relevant form field using aria-describedby (noting that this - attribute allows more than one id to be referenced, in case the field - already points to additional form text). -

- -
- - - - Looks good! - - - - - Looks good! - - - - - @ - - Please choose a username. - - - - - - Please provide a valid city. - - - - - Please provide a valid state. - - - - - Please provide a valid zip. - - - - - - - You must agree before submitting. - - - - -
-
-
-
-
- - - - Validation Supported elements - - -

- Validation styles are available for the following form controls and components: -

-
    -
  • - <input cFormControl> -
  • -
  • - <select cSelect> -
  • -
  • - <c-form-check> -
  • -
- -
-
- - - Please enter a message in the textarea. -
- - - - - Example invalid feedback text - - - - - - - - - - - - More example invalid feedback text - -
- - Example invalid select feedback -
- -
- - Example invalid form file feedback -
- -
- -
-
-
-
-
-
- - - - Validation Tooltips - - -

- If your form layout allows it, you can swap the text for the tooltip to display - validation feedback in a styled tooltip. Be sure to have a parent with - position: relative on it for tooltip positioning. In the example below, - our column classes have this already, but your project may require an alternative - setup. -

- - -
- - - - Looks good! - - - - - Looks good! - - - - - @ - - Please choose a username. - - - - - - Please provide a valid city. - - - - - Please provide a valid State. - - - - - Please provide a valid zip. - - - - - - - You must agree before submitting. - - - - - -
-
-
-
-
-
diff --git a/src/app/views/forms/validation/validation.component.scss b/src/app/views/forms/validation/validation.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/forms/validation/validation.component.spec.ts b/src/app/views/forms/validation/validation.component.spec.ts deleted file mode 100644 index 4a167905a..000000000 --- a/src/app/views/forms/validation/validation.component.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { ButtonModule, CardModule, FormModule, GridModule, ListGroupModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { ValidationComponent } from './validation.component'; - -describe('ValidationComponent', () => { - let component: ValidationComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [FormModule, ButtonModule, ListGroupModule, FormsModule, GridModule, CardModule, RouterTestingModule, ValidationComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(ValidationComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/forms/validation/validation.component.ts b/src/app/views/forms/validation/validation.component.ts deleted file mode 100644 index 848aafc42..000000000 --- a/src/app/views/forms/validation/validation.component.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { ReactiveFormsModule, FormsModule } from '@angular/forms'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, FormDirective, FormLabelDirective, FormControlDirective, FormFeedbackComponent, InputGroupComponent, InputGroupTextDirective, FormSelectDirective, FormCheckComponent, FormCheckInputDirective, FormCheckLabelDirective, ButtonDirective, ListGroupDirective, ListGroupItemDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-validation', - templateUrl: './validation.component.html', - styleUrls: ['./validation.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, ReactiveFormsModule, FormsModule, FormDirective, FormLabelDirective, FormControlDirective, FormFeedbackComponent, InputGroupComponent, InputGroupTextDirective, FormSelectDirective, FormCheckComponent, FormCheckInputDirective, FormCheckLabelDirective, ButtonDirective, ListGroupDirective, ListGroupItemDirective] -}) -export class ValidationComponent implements OnInit { - - customStylesValidated = false; - browserDefaultsValidated = false; - tooltipValidated = false; - - constructor() { } - - ngOnInit(): void { } - - onSubmit1() { - this.customStylesValidated = true; - console.log('Submit... 1'); - } - - onReset1() { - this.customStylesValidated = false; - console.log('Reset... 1'); - } - - onSubmit2() { - this.browserDefaultsValidated = true; - console.log('Submit... 2'); - } - - onReset2() { - this.browserDefaultsValidated = false; - console.log('Reset... 3'); - } - - onSubmit3() { - this.tooltipValidated = true; - console.log('Submit... 3'); - } - - onReset3() { - this.tooltipValidated = false; - console.log('Reset... 3'); - } - - -} diff --git a/src/app/views/icons/coreui-icons.component.html b/src/app/views/icons/coreui-icons.component.html deleted file mode 100644 index cec40ee84..000000000 --- a/src/app/views/icons/coreui-icons.component.html +++ /dev/null @@ -1,16 +0,0 @@ - - - {{ title }} - - - - - @for (icon of icons; track icon[0]) { - - -
{{ toKebabCase(icon[0]) }}
-
- } -
-
-
diff --git a/src/app/views/icons/coreui-icons.component.ts b/src/app/views/icons/coreui-icons.component.ts deleted file mode 100644 index 409405535..000000000 --- a/src/app/views/icons/coreui-icons.component.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; - -import { IconDirective, IconSetService } from '@coreui/icons-angular'; -import { brandSet, flagSet, freeSet } from '@coreui/icons'; -import { CardBodyComponent, CardComponent, CardHeaderComponent, ColComponent, RowComponent } from '@coreui/angular'; -import { DocsLinkComponent } from '@docs-components/public-api'; - -@Component({ - templateUrl: 'coreui-icons.component.html', - providers: [IconSetService], - standalone: true, - imports: [ - CardComponent, - CardHeaderComponent, - CardBodyComponent, - ColComponent, - DocsLinkComponent, - IconDirective, - RowComponent - ] -}) -export class CoreUIIconsComponent implements OnInit { - public title = 'CoreUI Icons'; - public icons!: [string, string[]][]; - - constructor( - private route: ActivatedRoute, public iconSet: IconSetService - ) { - iconSet.icons = { ...freeSet, ...brandSet, ...flagSet }; - } - - ngOnInit() { - const path = this.route?.routeConfig?.path; - let prefix = 'cil'; - if (path === 'coreui-icons') { - this.title = `${this.title} - Free`; - prefix = 'cil'; - } else if (path === 'brands') { - this.title = `${this.title} - Brands`; - prefix = 'cib'; - } else if (path === 'flags') { - this.title = `${this.title} - Flags`; - prefix = 'cif'; - } - this.icons = this.getIconsView(prefix); - } - - toKebabCase(str: string) { - return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase(); - } - - getIconsView(prefix: string) { - return Object.entries(this.iconSet.icons).filter((icon) => { - return icon[0].startsWith(prefix); - }); - } -} diff --git a/src/app/views/icons/routes.ts b/src/app/views/icons/routes.ts deleted file mode 100644 index d3c09ab59..000000000 --- a/src/app/views/icons/routes.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Routes } from '@angular/router'; - -export const routes: Routes = [ - { - path: '', - data: { - title: 'Icons' - }, - children: [ - { - path: '', - redirectTo: 'coreui-icons', - pathMatch: 'full' - }, - { - path: 'coreui-icons', - loadComponent: () => import('./coreui-icons.component').then(m => m.CoreUIIconsComponent), - data: { - title: 'CoreUI Icons' - } - }, - { - path: 'brands', - loadComponent: () => import('./coreui-icons.component').then(m => m.CoreUIIconsComponent), - data: { - title: 'Brands' - } - }, - { - path: 'flags', - loadComponent: () => import('./coreui-icons.component').then(m => m.CoreUIIconsComponent), - data: { - title: 'Flags' - } - } - ] - } -]; diff --git a/src/app/views/notifications/alerts/alerts.component.html b/src/app/views/notifications/alerts/alerts.component.html deleted file mode 100644 index 61d571e4f..000000000 --- a/src/app/views/notifications/alerts/alerts.component.html +++ /dev/null @@ -1,226 +0,0 @@ - - - - - Angular Alert - - -

- Angular Alert is prepared for any length of text, as well as an optional close button. - For a styling, use one of the required contextual color - props (e.g., primary). For inline dismissal, use the - - dismissing prop - - . -

- - A simple primary alert—check it out! - A simple secondary alert—check it out! - A simple success alert—check it out! - A simple danger alert—check it out! - A simple warning alert—check it out! - A simple info alert—check it out! - A simple light alert—check it out! - A simple dark alert—check it out! - -
-
-
- - - - Angular Alert solid variant - - - - A solid primary alert—check it out! - A solid secondary alert—check it out! - A solid success alert—check it out! - A solid danger alert—check it out! - A solid warning alert—check it out! - A solid info alert—check it out! - A solid light alert—check it out! - A solid dark alert—check it out! - - - - - - - - Angular Alert Link color - - -

- Use the cAlertLink directive to immediately give matching colored - links inside any alert. -

- - - A simple primary alert with an example link. Give - it a click if you like. - - - A simple secondary alert with an example link. - Give it a click if you like. - - - A simple success alert with an example link. Give - it a click if you like. - - - A simple danger alert with an example link. Give - it a click if you like. - - - A simple warning alert with an example link. Give - it a click if you like. - - - A simple info alert with an example link. Give it - a click if you like. - - - A simple light alert with an example link. Give it - a click if you like. - - - A simple dark alert with an example link. Give it - a click if you like. - - -
-
-
- - - - Angular Alert Additional content - - -

- Alert can also incorporate supplementary components & elements like heading, - paragraph, and divider. -

- - -

Well done!

-

- Aww yeah, you successfully read this important alert message. This example text is - going to run a bit longer so that you can see how spacing within an alert works - with this kind of content. -

-
-

- Whenever you need to, be sure to use margin utilities to keep things nice and - tidy. -

-
-
-
-
-
- - - - Angular Alert Icons - - - - - -
An example alert with an icon
-
- - - - -
An example alert with an icon
-
- -
- - - - - - - - - - - - - - - - - -
An example primary alert with an icon
-
- - - - -
An example success alert with an icon
-
- - - - -
An example warning alert with an icon
-
- - - - -
An example danger alert with an icon
-
-
-
-
-
- - - - Angular Alert Dismissing - - -

- Alerts can also be easily dismissed. Just add the dismissible prop. -

- - @if (visible[0]) { - - Go right ahead and click that dismiss over there on the right. - - } - - @if (alertWithButtonCloseTemplate.dismissible) { - - - - } - Go right ahead and click that dismiss over there on the right. - -
- - -
-
-
-
-
diff --git a/src/app/views/notifications/alerts/alerts.component.scss b/src/app/views/notifications/alerts/alerts.component.scss deleted file mode 100644 index cf9a4ce73..000000000 --- a/src/app/views/notifications/alerts/alerts.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -:host-context(.dark-theme) ::ng-deep .btn-close { - --cui-btn-close-bg: url("data:image/svg+xml,") !important; -} diff --git a/src/app/views/notifications/alerts/alerts.component.spec.ts b/src/app/views/notifications/alerts/alerts.component.spec.ts deleted file mode 100644 index adaa88c2a..000000000 --- a/src/app/views/notifications/alerts/alerts.component.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { AlertModule, ButtonModule, CardModule, GridModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { AlertsComponent } from './alerts.component'; - -describe('AlertsComponent', () => { - let component: AlertsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [AlertModule, ButtonModule, NoopAnimationsModule, GridModule, CardModule, RouterTestingModule, AlertsComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(AlertsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/notifications/alerts/alerts.component.ts b/src/app/views/notifications/alerts/alerts.component.ts deleted file mode 100644 index 9229f66dd..000000000 --- a/src/app/views/notifications/alerts/alerts.component.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { RouterLink } from '@angular/router'; -import { - AlertComponent, - AlertHeadingDirective, - AlertLinkDirective, - ButtonCloseDirective, - ButtonDirective, - CardBodyComponent, - CardComponent, - CardHeaderComponent, - ColComponent, - RowComponent, - TemplateIdDirective, - TextColorDirective, - ThemeDirective -} from '@coreui/angular'; -import { IconDirective } from '@coreui/icons-angular'; -import { DocsExampleComponent } from '@docs-components/public-api'; - -@Component({ - selector: 'app-alerts', - templateUrl: './alerts.component.html', - styleUrls: ['./alerts.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, AlertComponent, AlertLinkDirective, RouterLink, AlertHeadingDirective, IconDirective, TemplateIdDirective, ThemeDirective, ButtonCloseDirective, ButtonDirective] -}) -export class AlertsComponent implements OnInit { - - visible = [true, true]; - dismissible = true; - - constructor() { } - - ngOnInit(): void { - } - - onAlertVisibleChange(eventValue: any = this.visible) { - this.visible[1] = eventValue; - } - - onResetDismiss() { - this.visible = [true, true]; - } - - onToggleDismiss() { - this.dismissible = !this.dismissible; - } - -} diff --git a/src/app/views/notifications/badges/badges.component.html b/src/app/views/notifications/badges/badges.component.html deleted file mode 100644 index b2ac2fb3a..000000000 --- a/src/app/views/notifications/badges/badges.component.html +++ /dev/null @@ -1,172 +0,0 @@ - - - - - Angular Badges - - -

- Bootstrap badge scale to suit the size of the parent element by using relative font - sizing and em units. -

- -

- Example heading - New -

-

- Example heading - New -

-

- Example heading - New -

-

- Example heading - New -

-
- Example heading - New -
-
- Example heading - New -
-
-

- Badges can be used as part of links or buttons to provide a counter. -

- - - -

- Remark that depending on how you use them, badges may be complicated for users of - screen readers and related assistive technologies. -

-

- Unless the context is clear, consider including additional context with a visually - hidden piece of additional text. -

- - - -
-
-
- - - - AngularBadges Contextual variations - - -

- Add any of the below-mentioned color props to modify the presentation of - a badge. -

- - primary - success - danger - warning - info - light - dark - -
-
- - - Angular Badges Pill badges - - -

- Apply the shape="rounded-pill" prop to make badges rounded. -

- - - primary - - - success - - - danger - - - warning - - - info - - - light - - - dark - - -
-
- - - Angular Badges Positioned - - -

- Use position prop to modify a component and position it in the corner of a link or button. -

- - - -
- - -
-
-
- - - Angular Badges Indicator - - -

- You can also create more generic indicators without a counter using a few more utilities. -

- - - -
-
-
-
- diff --git a/src/app/views/notifications/badges/badges.component.scss b/src/app/views/notifications/badges/badges.component.scss deleted file mode 100644 index d32384ed6..000000000 --- a/src/app/views/notifications/badges/badges.component.scss +++ /dev/null @@ -1,10 +0,0 @@ -:host-context(.dark-theme) ::ng-deep c-badge { - &.bg-light:not([class*="dark:"]) { - color: var(--cui-body-bg); - } - - &.bg-light-gradient:not([class*="dark:"]) { - color: var(--cui-body-bg); - } -} - diff --git a/src/app/views/notifications/badges/badges.component.spec.ts b/src/app/views/notifications/badges/badges.component.spec.ts deleted file mode 100644 index 11b7cb42c..000000000 --- a/src/app/views/notifications/badges/badges.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { BadgeModule, ButtonModule, CardModule, GridModule, UtilitiesModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { BadgesComponent } from './badges.component'; - -describe('BadgesComponent', () => { - let component: BadgesComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [BadgeModule, CardModule, GridModule, UtilitiesModule, ButtonModule, RouterTestingModule, BadgesComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(BadgesComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/notifications/badges/badges.component.ts b/src/app/views/notifications/badges/badges.component.ts deleted file mode 100644 index 4ba8b8c41..000000000 --- a/src/app/views/notifications/badges/badges.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component } from '@angular/core'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, BadgeComponent, ButtonDirective, BorderDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-badges', - templateUrl: './badges.component.html', - styleUrls: ['./badges.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, BadgeComponent, ButtonDirective, BorderDirective] -}) -export class BadgesComponent { - - constructor() { } - -} diff --git a/src/app/views/notifications/modals/modals.component.html b/src/app/views/notifications/modals/modals.component.html deleted file mode 100644 index c8c0c0116..000000000 --- a/src/app/views/notifications/modals/modals.component.html +++ /dev/null @@ -1,582 +0,0 @@ - - - - - Angular Modal - - -

- Below is a static modal example (meaning its position and - display have been overridden). Included are the modal header, modal body - (required for padding), and modal footer (optional). We ask that you - include modal headers with dismiss actions whenever possible, or provide another - explicit dismiss action. -

- - - -
Modal title
- -
- Modal body text goes here. - - - - -
-
-
-
-
- - - - Angular Modal Live demo - - -

- Toggle a working modal demo by clicking the button below. It will slide down and fade - in from the top of the page. -

- - - -
-
-
- - - - Angular Modal Static backdrop - - -

- If you don’t provide an (visibleChange) handler to the Modal component, your - modal will behave as though the backdrop is static, meaning it will not close when - clicking outside it. Click the button below to try it. -

- - - -
-
-
- - - - Angular Modal Scrolling long content - - -

- If your modals are too long for the user’s viewport, they scroll the page by itself. -

- - - -

- You can also create a scrollable modal that allows scroll the modal body by adding scrollable - prop. -

- - - -
-
-
- - - - Angular Modal Vertically centered - - -

- Add alignment="center" to <c-modal> to - vertically center the modal. -

- - - - - - -
-
-
- - - - Angular Modal Tooltips and popovers - - -

- cTooltip and cPopover can be placed within - modals as needed. When modals are closed, any tooltips and popovers within are also - automatically dismissed. -

- - - -
-
-
- - - - Angular Modal Optional sizes - - -

- Modals have three optional sizes, available via modifier classes to be placed on a - <c-modal>. These sizes kick in at certain breakpoints to avoid - horizontal scrollbars on narrower viewports. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SizeProperty sizeModal max-width
Small - 'sm' - - 300px -
DefaultNone - 500px -
Large - 'lg' - - 800px -
Extra large - 'xl' - - 1140px -
- - - -
-
-
- - - - Angular Modal Fullscreen Modal - - -

- Another override is the option to pop up a modal that covers the user viewport, - available via property fullscreen. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Property fullscreenAvailability
- true - Always
- 'sm' - - Below 576px -
- 'md' - - Below 768px -
- 'lg' - - Below 992px -
- 'xl' - - Below 1200px -
- 'xxl' - - Below 1400px -
- - - -
-
-
-
- - - - - -
Modal title
- -
- Woohoo, you're reading this text in a modal! - - - - -
-
- - - - - -
Modal title
- -
- I will not close if you click outside of me. Don't even try to press escape key. - - - - -
-
- - - - - -
Modal title
- -
- - - - - - - -
-
- - - - - -
Modal title
- -
- - - - - - - -
-
- - - - - -
Modal title
- -
- - Woohoo, you're reading this text in a modal! - - - - - -
-
- - - - - -
Modal title
- -
- -

- This is some placeholder content to show a vertically centered modal. We've added some extra copy here to show - how vertically centering the modal works when combined with scrollable modals. We also use some repeated line - breaks to quickly extend the height of the content, thereby triggering the scrolling. When content becomes - longer than the predefined max-height of modal, content will be cropped and scrollable within the modal. -

-









-

Just like that.

-
- - - - -
-
- - - - - -
Modal title
- -
- -
Popover in a modal
- This - - triggers a popover on click. - -

- Popover title -

-
- And here’s some amazing content. It’s very engaging. Right? -
-
-
-
Tooltips in a modal
-

- This link and that link - have tooltips on hover. -

-
- - - - -
-
- - - - - - - -
Extra large modal
-
- ... -
- - -
Large modal
-
- ... -
- - -
Small modal
-
- ... -
-
- - - - - - - - - - -
Full screen
- -
- ... - - - -
- - -
Full screen below sm
- -
- ... -
- - -
Full screen below md
- -
- ... -
- - -
Full screen below lg
- -
- ... -
- - -
Full screen below xl
- -
- ... -
- - -
Full screen below xxl
- -
- ... -
-
- - -

- Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis - in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. -

-

- Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis - lacus vel augue laoreet rutrum faucibus dolor auctor. -

-

- Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel - scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus - auctor fringilla. -

-

- Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis - in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. -

-

- Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis - lacus vel augue laoreet rutrum faucibus dolor auctor. -

-

- Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel - scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus - auctor fringilla. -

-

- Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis - in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. -

-

- Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis - lacus vel augue laoreet rutrum faucibus dolor auctor. -

-

- Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel - scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus - auctor fringilla. -

-

- Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis - in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. -

-

- Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis - lacus vel augue laoreet rutrum faucibus dolor auctor. -

-

- Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel - scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus - auctor fringilla. -

-

- Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis - in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. -

-

- Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis - lacus vel augue laoreet rutrum faucibus dolor auctor. -

-

- Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel - scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus - auctor fringilla. -

-

- Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis - in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. -

-

- Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis - lacus vel augue laoreet rutrum faucibus dolor auctor. -

-

- Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel - scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus - auctor fringilla. -

-
diff --git a/src/app/views/notifications/modals/modals.component.scss b/src/app/views/notifications/modals/modals.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/notifications/modals/modals.component.spec.ts b/src/app/views/notifications/modals/modals.component.spec.ts deleted file mode 100644 index f673a39d9..000000000 --- a/src/app/views/notifications/modals/modals.component.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { ButtonModule, CardModule, GridModule, ModalModule, PopoverModule, TooltipModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { ModalsComponent } from './modals.component'; - -describe('ModalsComponent', () => { - let component: ModalsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ModalModule, NoopAnimationsModule, GridModule, CardModule, PopoverModule, ButtonModule, RouterTestingModule, TooltipModule, ModalsComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(ModalsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/notifications/modals/modals.component.ts b/src/app/views/notifications/modals/modals.component.ts deleted file mode 100644 index bbe662aaf..000000000 --- a/src/app/views/notifications/modals/modals.component.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Component } from '@angular/core'; -import { NgTemplateOutlet } from '@angular/common'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { - ButtonCloseDirective, - ButtonDirective, - CardBodyComponent, - CardComponent, - CardHeaderComponent, - ColComponent, - ModalBodyComponent, - ModalComponent, - ModalFooterComponent, - ModalHeaderComponent, - ModalTitleDirective, - ModalToggleDirective, - PopoverDirective, - RowComponent, - TextColorDirective, - ThemeDirective, - TooltipDirective -} from '@coreui/angular'; - -@Component({ - selector: 'app-modals', - templateUrl: './modals.component.html', - styleUrls: ['./modals.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, ModalComponent, ModalHeaderComponent, ModalTitleDirective, ThemeDirective, ButtonCloseDirective, ModalBodyComponent, ModalFooterComponent, ButtonDirective, NgTemplateOutlet, ModalToggleDirective, PopoverDirective, TooltipDirective] -}) -export class ModalsComponent { - - public liveDemoVisible = false; - - toggleLiveDemo() { - this.liveDemoVisible = !this.liveDemoVisible; - } - - handleLiveDemoChange(event: boolean) { - this.liveDemoVisible = event; - } -} diff --git a/src/app/views/notifications/routes.ts b/src/app/views/notifications/routes.ts deleted file mode 100644 index 27905cecf..000000000 --- a/src/app/views/notifications/routes.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Routes } from '@angular/router'; - -export const routes: Routes = [ - { - path: '', - data: { - title: 'Notifications' - }, - children: [ - { - path: '', - redirectTo: 'badges', - pathMatch: 'full' - }, - { - path: 'alerts', - loadComponent: () => import('./alerts/alerts.component').then(m => m.AlertsComponent), - data: { - title: 'Alerts' - } - }, - { - path: 'badges', - loadComponent: () => import('./badges/badges.component').then(m => m.BadgesComponent), - data: { - title: 'Badges' - } - }, - { - path: 'modal', - loadComponent: () => import('./modals/modals.component').then(m => m.ModalsComponent), - data: { - title: 'Modal' - } - }, - { - path: 'toasts', - loadComponent: () => import('./toasters/toasters.component').then(m => m.ToastersComponent), - data: { - title: 'Toasts' - } - } - ] - } -]; diff --git a/src/app/views/notifications/toasters/toast-simple/toast-sample-icon.component.svg b/src/app/views/notifications/toasters/toast-simple/toast-sample-icon.component.svg deleted file mode 100644 index 64a6cdc25..000000000 --- a/src/app/views/notifications/toasters/toast-simple/toast-sample-icon.component.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/src/app/views/notifications/toasters/toast-simple/toast-sample-icon.component.ts b/src/app/views/notifications/toasters/toast-simple/toast-sample-icon.component.ts deleted file mode 100644 index e55408b72..000000000 --- a/src/app/views/notifications/toasters/toast-simple/toast-sample-icon.component.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Component, OnInit } from '@angular/core'; - -@Component({ - selector: 'toast-sample-icon', - templateUrl: './toast-sample-icon.component.svg', -}) -export class ToastSampleIconComponent implements OnInit { - - constructor() { } - - ngOnInit(): void { - } - -} diff --git a/src/app/views/notifications/toasters/toast-simple/toast.component.html b/src/app/views/notifications/toasters/toast-simple/toast.component.html deleted file mode 100644 index 0e3ec8fe6..000000000 --- a/src/app/views/notifications/toasters/toast-simple/toast.component.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - {{ title }} - - -

This is a dynamic toast no {{ toastBody.toast?.index }} {{ toastBody.toast?.clock }}

- - -
-
diff --git a/src/app/views/notifications/toasters/toast-simple/toast.component.scss b/src/app/views/notifications/toasters/toast-simple/toast.component.scss deleted file mode 100644 index 20947d5cd..000000000 --- a/src/app/views/notifications/toasters/toast-simple/toast.component.scss +++ /dev/null @@ -1,4 +0,0 @@ -:host { - display: block; - overflow: hidden; -} diff --git a/src/app/views/notifications/toasters/toast-simple/toast.component.spec.ts b/src/app/views/notifications/toasters/toast-simple/toast.component.spec.ts deleted file mode 100644 index d51204015..000000000 --- a/src/app/views/notifications/toasters/toast-simple/toast.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; - -import { ButtonModule, ProgressModule, ToastModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../../icons/icon-subset'; -import { AppToastComponent } from './toast.component'; - -describe('ToastComponent', () => { - let component: AppToastComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, ToastModule, ProgressModule, ButtonModule, AppToastComponent], - providers: [IconSetService] -}) - .compileComponents(); - })); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(AppToastComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/notifications/toasters/toast-simple/toast.component.ts b/src/app/views/notifications/toasters/toast-simple/toast.component.ts deleted file mode 100644 index b7ad1c808..000000000 --- a/src/app/views/notifications/toasters/toast-simple/toast.component.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ChangeDetectorRef, Component, ElementRef, forwardRef, Input, Renderer2 } from '@angular/core'; - -import { ToastComponent, ToasterService, ToastHeaderComponent, ToastBodyComponent, ToastCloseDirective, ProgressBarDirective, ProgressComponent } from '@coreui/angular'; - -@Component({ - selector: 'app-toast-simple', - templateUrl: './toast.component.html', - styleUrls: ['./toast.component.scss'], - providers: [{ provide: ToastComponent, useExisting: forwardRef(() => AppToastComponent) }], - standalone: true, - imports: [ToastHeaderComponent, ToastBodyComponent, ToastCloseDirective, ProgressBarDirective, ProgressComponent] -}) -export class AppToastComponent extends ToastComponent { - - @Input() closeButton = true; - @Input() title = ''; - - constructor( - public override hostElement: ElementRef, - public override renderer: Renderer2, - public override toasterService: ToasterService, - public override changeDetectorRef: ChangeDetectorRef - ) { - super(hostElement, renderer, toasterService, changeDetectorRef); - } -} diff --git a/src/app/views/notifications/toasters/toasters.component.html b/src/app/views/notifications/toasters/toasters.component.html deleted file mode 100644 index 73163f32a..000000000 --- a/src/app/views/notifications/toasters/toasters.component.html +++ /dev/null @@ -1,96 +0,0 @@ - - - @for (pos of positions | slice : 1; track pos) { - - } - - - Toaster - - - - - -
-
Add toast with following props:
- - - - - - - - Delay - - - - - Position - - - - - Color - - - - - - - - - - - - -
- -
-
- - - - Toast title - This is a static toast in a static toaster - - - Toast title - This is a static toast in a static toaster - - - This is a toast in static positioned App toaster! {{ toast.index }} - - - -
-
-
-
- - - - -

Form value: {{ toasterForm.value | json }}

-
-
-
-
-
-
diff --git a/src/app/views/notifications/toasters/toasters.component.scss b/src/app/views/notifications/toasters/toasters.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/notifications/toasters/toasters.component.spec.ts b/src/app/views/notifications/toasters/toasters.component.spec.ts deleted file mode 100644 index 5765f4a93..000000000 --- a/src/app/views/notifications/toasters/toasters.component.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; - -import { ButtonModule, CardModule, FormModule, GridModule, ProgressModule, ToastModule } from '@coreui/angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { ToastersComponent } from './toasters.component'; -import { AppToastComponent } from './toast-simple/toast.component'; - -describe('ToastersComponent', () => { - let component: ToastersComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, GridModule, ToastModule, CardModule, FormModule, ButtonModule, ProgressModule, FormsModule, ReactiveFormsModule, ToastersComponent, AppToastComponent], - providers: [IconSetService] -}) - .compileComponents(); - })); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(ToastersComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/notifications/toasters/toasters.component.ts b/src/app/views/notifications/toasters/toasters.component.ts deleted file mode 100644 index 047e53730..000000000 --- a/src/app/views/notifications/toasters/toasters.component.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { JsonPipe, NgClass, NgStyle, SlicePipe } from '@angular/common'; -import { Component, OnInit, QueryList, ViewChildren } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { ReactiveFormsModule, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; -import { Observable } from 'rxjs'; -import { filter } from 'rxjs/operators'; - -import { - ButtonDirective, - CardBodyComponent, - CardComponent, - CardHeaderComponent, - ColComponent, - ContainerComponent, - FormCheckComponent, - FormCheckInputDirective, - FormCheckLabelDirective, - FormControlDirective, - FormDirective, - FormSelectDirective, - InputGroupComponent, - InputGroupTextDirective, - RowComponent, - TextColorDirective, - ToastBodyComponent, - ToastComponent, - ToasterComponent, - ToasterPlacement, - ToastHeaderComponent -} from '@coreui/angular'; -import { AppToastComponent } from './toast-simple/toast.component'; - -export enum Colors { - '' = '', - primary = 'primary', - secondary = 'secondary', - success = 'success', - info = 'info', - warning = 'warning', - danger = 'danger', - dark = 'dark', - light = 'light', -} - -@Component({ - selector: 'app-toasters', - templateUrl: './toasters.component.html', - styleUrls: ['./toasters.component.scss'], - standalone: true, - imports: [RowComponent, ColComponent, ToasterComponent, NgClass, TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, ContainerComponent, ReactiveFormsModule, FormDirective, FormCheckComponent, FormCheckInputDirective, FormCheckLabelDirective, InputGroupComponent, InputGroupTextDirective, FormControlDirective, FormSelectDirective, ButtonDirective, NgStyle, ToastComponent, ToastHeaderComponent, ToastBodyComponent, AppToastComponent, JsonPipe, SlicePipe, TextColorDirective] -}) -export class ToastersComponent implements OnInit { - - positions = Object.values(ToasterPlacement); - position = ToasterPlacement.TopEnd; - positionStatic = ToasterPlacement.Static; - colors = Object.keys(Colors); - autohide = true; - delay = 5000; - fade = true; - - toasterForm = new UntypedFormGroup({ - autohide: new UntypedFormControl(this.autohide), - delay: new UntypedFormControl({ value: this.delay, disabled: !this.autohide }), - position: new UntypedFormControl(this.position), - fade: new UntypedFormControl({ value: true, disabled: false }), - closeButton: new UntypedFormControl(true), - color: new UntypedFormControl('') - }); - - formChanges: Observable = this.toasterForm.valueChanges.pipe( - takeUntilDestroyed(), - filter(e => e.autohide !== this.autohide) - ); - - @ViewChildren(ToasterComponent) viewChildren!: QueryList; - - ngOnInit(): void { - this.formChanges.subscribe(e => { - this.autohide = e.autohide; - this.position = e.position; - this.fade = e.fade; - const control = this.toasterForm?.get('delay'); - this.autohide ? control?.enable() : control?.disable(); - this.delay = control?.enabled ? e.timeout : this.delay; - }); - } - - addToast() { - const formValues = this.toasterForm.value; - const toasterPosition = this.viewChildren.filter(item => item.placement === this.toasterForm.value.position); - toasterPosition.forEach((item) => { - const title = `Toast ${formValues.color} ${formValues.position}`; - const { ...props } = { ...formValues, title }; - const componentRef = item.addToast(AppToastComponent, props, {}); - componentRef.instance['closeButton'] = props.closeButton; - }); - } -} diff --git a/src/app/views/pages/login/login.component.html b/src/app/views/pages/login/login.component.html deleted file mode 100644 index 2f1645ee9..000000000 --- a/src/app/views/pages/login/login.component.html +++ /dev/null @@ -1,61 +0,0 @@ -
- - - - - - -
-

Login

-

Sign In to your account

- - - - - - - - - - - - - - - - - - - - -
-
-
- - -
-

Sign up

-

- Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod - tempor incididunt ut labore et dolore magna aliqua. -

- -
-
-
-
-
-
-
-
diff --git a/src/app/views/pages/login/login.component.scss b/src/app/views/pages/login/login.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/pages/login/login.component.ts b/src/app/views/pages/login/login.component.ts deleted file mode 100644 index b6a70e77d..000000000 --- a/src/app/views/pages/login/login.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Component } from '@angular/core'; -import { NgStyle } from '@angular/common'; -import { IconDirective } from '@coreui/icons-angular'; -import { ContainerComponent, RowComponent, ColComponent, CardGroupComponent, TextColorDirective, CardComponent, CardBodyComponent, FormDirective, InputGroupComponent, InputGroupTextDirective, FormControlDirective, ButtonDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-login', - templateUrl: './login.component.html', - styleUrls: ['./login.component.scss'], - standalone: true, - imports: [ContainerComponent, RowComponent, ColComponent, CardGroupComponent, TextColorDirective, CardComponent, CardBodyComponent, FormDirective, InputGroupComponent, InputGroupTextDirective, IconDirective, FormControlDirective, ButtonDirective, NgStyle] -}) -export class LoginComponent { - - constructor() { } - -} diff --git a/src/app/views/pages/page404/page404.component.html b/src/app/views/pages/page404/page404.component.html deleted file mode 100644 index c98af9878..000000000 --- a/src/app/views/pages/page404/page404.component.html +++ /dev/null @@ -1,22 +0,0 @@ -
- - - -
-

404

-

Oops! You're lost.

-

- The page you are looking for was not found. -

-
- - - - - - - -
-
-
-
diff --git a/src/app/views/pages/page404/page404.component.scss b/src/app/views/pages/page404/page404.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/pages/page404/page404.component.spec.ts b/src/app/views/pages/page404/page404.component.spec.ts deleted file mode 100644 index 0c0a7561d..000000000 --- a/src/app/views/pages/page404/page404.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ButtonModule, FormModule, GridModule } from '@coreui/angular'; -import { IconModule } from '@coreui/icons-angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { Page404Component } from './page404.component'; - -describe('Page404Component', () => { - let component: Page404Component; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [FormModule, GridModule, ButtonModule, IconModule, Page404Component], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(Page404Component); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/pages/page404/page404.component.ts b/src/app/views/pages/page404/page404.component.ts deleted file mode 100644 index 91d22d6a5..000000000 --- a/src/app/views/pages/page404/page404.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component } from '@angular/core'; -import { IconDirective } from '@coreui/icons-angular'; -import { ContainerComponent, RowComponent, ColComponent, InputGroupComponent, InputGroupTextDirective, FormControlDirective, ButtonDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-page404', - templateUrl: './page404.component.html', - styleUrls: ['./page404.component.scss'], - standalone: true, - imports: [ContainerComponent, RowComponent, ColComponent, InputGroupComponent, InputGroupTextDirective, IconDirective, FormControlDirective, ButtonDirective] -}) -export class Page404Component { - - constructor() { } - -} diff --git a/src/app/views/pages/page500/page500.component.html b/src/app/views/pages/page500/page500.component.html deleted file mode 100644 index d710779e1..000000000 --- a/src/app/views/pages/page500/page500.component.html +++ /dev/null @@ -1,22 +0,0 @@ -
- - - - -

500

-

Houston, we have a problem!

-

- The page you are looking for is temporarily unavailable. -

-
- - - - - - - -
-
-
-
diff --git a/src/app/views/pages/page500/page500.component.scss b/src/app/views/pages/page500/page500.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/pages/page500/page500.component.spec.ts b/src/app/views/pages/page500/page500.component.spec.ts deleted file mode 100644 index 80839ad88..000000000 --- a/src/app/views/pages/page500/page500.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ButtonModule, FormModule, GridModule } from '@coreui/angular'; -import { IconModule } from '@coreui/icons-angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { Page500Component } from './page500.component'; - -describe('Page500Component', () => { - let component: Page500Component; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [GridModule, ButtonModule, FormModule, IconModule, Page500Component], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(Page500Component); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/pages/page500/page500.component.ts b/src/app/views/pages/page500/page500.component.ts deleted file mode 100644 index 33632cd66..000000000 --- a/src/app/views/pages/page500/page500.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component } from '@angular/core'; -import { IconDirective } from '@coreui/icons-angular'; -import { ContainerComponent, RowComponent, ColComponent, InputGroupComponent, InputGroupTextDirective, FormControlDirective, ButtonDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-page500', - templateUrl: './page500.component.html', - styleUrls: ['./page500.component.scss'], - standalone: true, - imports: [ContainerComponent, RowComponent, ColComponent, InputGroupComponent, InputGroupTextDirective, IconDirective, FormControlDirective, ButtonDirective] -}) -export class Page500Component { - - constructor() { } - -} diff --git a/src/app/views/pages/register/register.component.html b/src/app/views/pages/register/register.component.html deleted file mode 100644 index 68bffd177..000000000 --- a/src/app/views/pages/register/register.component.html +++ /dev/null @@ -1,41 +0,0 @@ -
- - - - - -
-

Register

-

Create your account

- - - - - - - - @ - - - - - - - - - - - - - - -
- -
-
-
-
-
-
-
-
diff --git a/src/app/views/pages/register/register.component.scss b/src/app/views/pages/register/register.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/pages/register/register.component.spec.ts b/src/app/views/pages/register/register.component.spec.ts deleted file mode 100644 index 61d63d08a..000000000 --- a/src/app/views/pages/register/register.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ButtonModule, CardModule, FormModule, GridModule } from '@coreui/angular'; -import { IconModule } from '@coreui/icons-angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { RegisterComponent } from './register.component'; - -describe('RegisterComponent', () => { - let component: RegisterComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CardModule, FormModule, GridModule, ButtonModule, IconModule, RegisterComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(RegisterComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/pages/register/register.component.ts b/src/app/views/pages/register/register.component.ts deleted file mode 100644 index df0059d18..000000000 --- a/src/app/views/pages/register/register.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component } from '@angular/core'; -import { IconDirective } from '@coreui/icons-angular'; -import { ContainerComponent, RowComponent, ColComponent, TextColorDirective, CardComponent, CardBodyComponent, FormDirective, InputGroupComponent, InputGroupTextDirective, FormControlDirective, ButtonDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-register', - templateUrl: './register.component.html', - styleUrls: ['./register.component.scss'], - standalone: true, - imports: [ContainerComponent, RowComponent, ColComponent, TextColorDirective, CardComponent, CardBodyComponent, FormDirective, InputGroupComponent, InputGroupTextDirective, IconDirective, FormControlDirective, ButtonDirective] -}) -export class RegisterComponent { - - constructor() { } - -} diff --git a/src/app/views/pages/routes.ts b/src/app/views/pages/routes.ts deleted file mode 100644 index 410f516f7..000000000 --- a/src/app/views/pages/routes.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Routes } from '@angular/router'; - -export const routes: Routes = [ - { - path: '404', - loadComponent: () => import('./page404/page404.component').then(m => m.Page404Component), - data: { - title: 'Page 404' - } - }, - { - path: '500', - loadComponent: () => import('./page500/page500.component').then(m => m.Page500Component), - data: { - title: 'Page 500' - } - }, - { - path: 'login', - loadComponent: () => import('./login/login.component').then(m => m.LoginComponent), - data: { - title: 'Login Page' - } - }, - { - path: 'register', - loadComponent: () => import('./register/register.component').then(m => m.RegisterComponent), - data: { - title: 'Register Page' - } - } -]; diff --git a/src/app/views/theme/colors.component.html b/src/app/views/theme/colors.component.html deleted file mode 100644 index 159b16bc2..000000000 --- a/src/app/views/theme/colors.component.html +++ /dev/null @@ -1,33 +0,0 @@ - - - Theme colors - - - - -
Brand Primary Color
-
- -
Brand Secondary Color
-
- -
Brand Success Color
-
- -
Brand Danger Color
-
- -
Brand Warning Color
-
- -
Brand Info Color
-
- -
Brand Light Color
-
- -
Brand Dark Color
-
-
-
-
diff --git a/src/app/views/theme/colors.component.ts b/src/app/views/theme/colors.component.ts deleted file mode 100644 index 855b80b08..000000000 --- a/src/app/views/theme/colors.component.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { AfterViewInit, Component, HostBinding, Inject, Input, OnInit, Renderer2, forwardRef } from '@angular/core'; -import { DOCUMENT, NgClass } from '@angular/common'; - -import { getStyle, rgbToHex } from '@coreui/utils'; -import { TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, RowComponent, ColComponent } from '@coreui/angular'; - -@Component({ - templateUrl: 'colors.component.html', - standalone: true, - imports: [TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, RowComponent, forwardRef(() => ThemeColorComponent)] -}) -export class ColorsComponent implements OnInit, AfterViewInit { - - constructor( - @Inject(DOCUMENT) private document: Document, - private renderer: Renderer2 - ) { - } - - public themeColors(): void { - Array.from(this.document.querySelectorAll('.theme-color')).forEach( - (element: Element) => { - const htmlElement = element as HTMLElement; - const background = getStyle('background-color', htmlElement) ?? '#fff'; - const table = this.renderer.createElement('table'); - table.innerHTML = ` - - - - - - - - - -
HEX:${rgbToHex(background)}
RGB:${background}
- `; - this.renderer.appendChild(htmlElement.parentNode, table); - // @ts-ignore - // el.parentNode.appendChild(table); - } - ); - } - - ngOnInit(): void {} - - ngAfterViewInit(): void { - this.themeColors(); - } -} - -@Component({ - selector: 'app-theme-color', - template: ` - -
- -
- `, - standalone: true, - imports: [ColComponent, NgClass], -}) -export class ThemeColorComponent implements OnInit { - @Input() color = ''; - public colorClasses = { - 'theme-color w-75 rounded mb-3': true - }; - - @HostBinding('style.display') display = 'contents'; - - ngOnInit(): void { - this.colorClasses = { - ...this.colorClasses, - [`bg-${this.color}`]: !!this.color - }; - } -} - diff --git a/src/app/views/theme/routes.ts b/src/app/views/theme/routes.ts deleted file mode 100644 index 44f2aa98b..000000000 --- a/src/app/views/theme/routes.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Routes } from '@angular/router'; - -export const routes: Routes = [ - { - path: '', - data: { - title: 'Theme' - }, - children: [ - { - path: '', - redirectTo: 'colors', - pathMatch: 'full' - }, - { - path: 'colors', - loadComponent: () => import('./colors.component').then(m => m.ColorsComponent), - data: { - title: 'Colors' - } - }, - { - path: 'typography', - loadComponent: () => import('./typography.component').then(m => m.TypographyComponent), - data: { - title: 'Typography' - } - } - ] - } -]; - diff --git a/src/app/views/theme/typography.component.html b/src/app/views/theme/typography.component.html deleted file mode 100644 index 2bea088f5..000000000 --- a/src/app/views/theme/typography.component.html +++ /dev/null @@ -1,166 +0,0 @@ - - - Headings - - -

Documentation and examples for Bootstrap typography, including global settings, headings, body text, lists, and - more.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
HeadingExample
-

<h1></h1>

-

h1. Bootstrap heading

-

<h2></h2>

-

h2. Bootstrap heading

-

<h3></h3>

-

h3. Bootstrap heading

-

<h4></h4>

-

h4. Bootstrap heading

-

<h5></h5>

-
h5. Bootstrap heading
-

<h6></h6>

-
h6. Bootstrap heading
-
-
- - - Headings - - -

.h1 through .h6 classes are - also available, for when you want to match the font styling of a heading but cannot use the associated HTML - element.

-
-

h1. Bootstrap heading

-

h2. Bootstrap heading

-

h3. Bootstrap heading

-

h4. Bootstrap heading

-

h5. Bootstrap heading

-

h6. Bootstrap heading

-
-
-
- - - Display headings - - -

Traditional heading elements are designed to work best in the meat of your page content. When you need a heading - to stand out, consider using a display heading—a larger, slightly more opinionated heading style. -

-
- - - - - - - - - - - - - - - -
Display 1
Display 2
Display 3
Display 4
-
-
-
- - - Inline text elements - - -

Traditional heading elements are designed to work best in the meat of your page content. When you need a heading - to stand out, consider using a display heading—a larger, slightly more opinionated heading style. -

-
-

You can use the mark tag to - highlight - text. -

-

- This line of text is meant to be treated as deleted text. -

-

This line of text is meant to be treated as no longer accurate.

-

- This line of text is meant to be treated as an addition to the document. -

-

This line of text will render as underlined

-

This line of text is meant to be treated as fine print.

-

This line rendered as bold text.

-

This line rendered as italicized text.

-
-
-
- - - Description list alignment - - -

Align terms and descriptions horizontally by using our grid system’s predefined classes (or semantic mixins). For - longer terms, you can optionally add a .text-truncate class to truncate - the text with an ellipsis.

-
-
-
Description lists
-
A description list is perfect for defining terms.
- -
Euismod
-
-

Vestibulum id ligula porta felis euismod semper eget lacinia odio sem nec elit.

-

Donec id elit non mi porta gravida at eget metus.

-
- -
Malesuada porta
-
Etiam porta sem malesuada magna mollis euismod.
- -
Truncated term is truncated with d-block
-
Fusce dapibus, tellus ac cursus commodo, tortor mauris - condimentum nibh, ut fermentum massa justo sit amet risus. -
- -
Nesting
-
-
-
Nested definition list
-
Aenean posuere, tortor sed cursus feugiat, nunc augue blandit nunc.
-
-
-
-
-
-
diff --git a/src/app/views/theme/typography.component.ts b/src/app/views/theme/typography.component.ts deleted file mode 100644 index dff202815..000000000 --- a/src/app/views/theme/typography.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component } from '@angular/core'; -import { TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent } from '@coreui/angular'; - -@Component({ - templateUrl: 'typography.component.html', - standalone: true, - imports: [ - TextColorDirective, - CardComponent, - CardHeaderComponent, - CardBodyComponent, - ], -}) -export class TypographyComponent { - constructor() {} -} diff --git a/src/app/views/widgets/widgets-brand/widgets-brand.component.html b/src/app/views/widgets/widgets-brand/widgets-brand.component.html deleted file mode 100644 index 03608cda8..000000000 --- a/src/app/views/widgets/widgets-brand/widgets-brand.component.html +++ /dev/null @@ -1,24 +0,0 @@ - - @for (widget of brandData; track widget; let i = $index) { - - - - @if (withCharts) { - - {{ chart.id }} - - } - - - } - diff --git a/src/app/views/widgets/widgets-brand/widgets-brand.component.scss b/src/app/views/widgets/widgets-brand/widgets-brand.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/widgets/widgets-brand/widgets-brand.component.spec.ts b/src/app/views/widgets/widgets-brand/widgets-brand.component.spec.ts deleted file mode 100644 index 0e3d69441..000000000 --- a/src/app/views/widgets/widgets-brand/widgets-brand.component.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { GridModule, WidgetModule } from '@coreui/angular'; -import { ChartjsModule } from '@coreui/angular-chartjs'; -import { IconModule } from '@coreui/icons-angular'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { WidgetsBrandComponent } from './widgets-brand.component'; - -describe('WidgetsBrandComponent', () => { - let component: WidgetsBrandComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [WidgetModule, GridModule, ChartjsModule, IconModule, WidgetsBrandComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(WidgetsBrandComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/widgets/widgets-brand/widgets-brand.component.ts b/src/app/views/widgets/widgets-brand/widgets-brand.component.ts deleted file mode 100644 index b01168b8b..000000000 --- a/src/app/views/widgets/widgets-brand/widgets-brand.component.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, Input } from '@angular/core'; -import { ChartjsComponent } from '@coreui/angular-chartjs'; -import { IconDirective } from '@coreui/icons-angular'; -import { ColComponent, RowComponent, WidgetStatDComponent } from '@coreui/angular'; -import { ChartData } from 'chart.js'; - -type BrandData = { - icon: string - values: any[] - capBg?: any - color?: string - labels?: string[] - data: ChartData -} - -@Component({ - selector: 'app-widgets-brand', - templateUrl: './widgets-brand.component.html', - styleUrls: ['./widgets-brand.component.scss'], - changeDetection: ChangeDetectionStrategy.Default, - standalone: true, - imports: [RowComponent, ColComponent, WidgetStatDComponent, IconDirective, ChartjsComponent] -}) -export class WidgetsBrandComponent implements AfterContentInit { - - constructor( - private changeDetectorRef: ChangeDetectorRef - ) {} - - @Input() withCharts?: boolean; - // @ts-ignore - chartOptions = { - elements: { - line: { - tension: 0.4 - }, - point: { - radius: 0, - hitRadius: 10, - hoverRadius: 4, - hoverBorderWidth: 3 - } - }, - maintainAspectRatio: false, - plugins: { - legend: { - display: false - } - }, - scales: { - x: { - display: false - }, - y: { - display: false - } - } - }; - labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July']; - datasets = { - borderWidth: 2, - fill: true - }; - colors = { - backgroundColor: 'rgba(255,255,255,.1)', - borderColor: 'rgba(255,255,255,.55)', - pointHoverBackgroundColor: '#fff', - pointBackgroundColor: 'rgba(255,255,255,.55)' - }; - brandData: BrandData[] = [ - { - icon: 'cibFacebook', - values: [{ title: 'friends', value: '89K' }, { title: 'feeds', value: '459' }], - capBg: { '--cui-card-cap-bg': '#3b5998' }, - labels: [...this.labels], - data: { - labels: [...this.labels], - datasets: [{ ...this.datasets, data: [65, 59, 84, 84, 51, 55, 40], label: 'Facebook', ...this.colors }] - } - }, - { - icon: 'cibTwitter', - values: [{ title: 'followers', value: '973k' }, { title: 'tweets', value: '1.792' }], - capBg: { '--cui-card-cap-bg': '#00aced' }, - data: { - labels: [...this.labels], - datasets: [{ ...this.datasets, data: [1, 13, 9, 17, 34, 41, 38], label: 'Twitter', ...this.colors }] - } - }, - { - icon: 'cib-linkedin', - values: [{ title: 'contacts', value: '500' }, { title: 'feeds', value: '1.292' }], - capBg: { '--cui-card-cap-bg': '#4875b4' }, - data: { - labels: [...this.labels], - datasets: [{ ...this.datasets, data: [78, 81, 80, 45, 34, 12, 40], label: 'LinkedIn', ...this.colors }] - } - }, - { - icon: 'cilCalendar', - values: [{ title: 'events', value: '12+' }, { title: 'meetings', value: '4' }], - color: 'warning', - data: { - labels: [...this.labels], - datasets: [{ ...this.datasets, data: [35, 23, 56, 22, 97, 23, 64], label: 'Events', ...this.colors }] - } - } - ]; - - capStyle(value: string) { - return !!value ? { '--cui-card-cap-bg': value } : {}; - } - - ngAfterContentInit(): void { - this.changeDetectorRef.detectChanges(); - } -} diff --git a/src/app/views/widgets/widgets-dropdown/widgets-dropdown.component.html b/src/app/views/widgets/widgets-dropdown/widgets-dropdown.component.html deleted file mode 100644 index 474cb2526..000000000 --- a/src/app/views/widgets/widgets-dropdown/widgets-dropdown.component.html +++ /dev/null @@ -1,123 +0,0 @@ - - - - - 26K - - (-12.4% ) - - - - - - - - - - - - - - - - - $6.200 - - (40.9%) - - - - - - - - - - - - - - - - - 2.49 - - (84.7% ) - - - - - - - - - - - - - - - - - 44K - - (-23.6% ) - - - - - - - - - - - - - - diff --git a/src/app/views/widgets/widgets-dropdown/widgets-dropdown.component.scss b/src/app/views/widgets/widgets-dropdown/widgets-dropdown.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/widgets/widgets-dropdown/widgets-dropdown.component.spec.ts b/src/app/views/widgets/widgets-dropdown/widgets-dropdown.component.spec.ts deleted file mode 100644 index 5a3cff5be..000000000 --- a/src/app/views/widgets/widgets-dropdown/widgets-dropdown.component.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ButtonModule, DropdownModule, GridModule, WidgetModule } from '@coreui/angular'; -import { IconModule } from '@coreui/icons-angular'; -import { ChartjsModule } from '@coreui/angular-chartjs'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { WidgetsDropdownComponent } from './widgets-dropdown.component'; -import { RouterTestingModule } from '@angular/router/testing'; - -describe('WidgetsDropdownComponent', () => { - let component: WidgetsDropdownComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [WidgetModule, DropdownModule, IconModule, ButtonModule, ChartjsModule, GridModule, WidgetsDropdownComponent, RouterTestingModule], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(WidgetsDropdownComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/widgets/widgets-dropdown/widgets-dropdown.component.ts b/src/app/views/widgets/widgets-dropdown/widgets-dropdown.component.ts deleted file mode 100644 index abb36e6bf..000000000 --- a/src/app/views/widgets/widgets-dropdown/widgets-dropdown.component.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { - AfterContentInit, - AfterViewInit, - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - OnInit, - ViewChild -} from '@angular/core'; -import { getStyle } from '@coreui/utils'; -import { ChartjsComponent } from '@coreui/angular-chartjs'; -import { RouterLink } from '@angular/router'; -import { IconDirective } from '@coreui/icons-angular'; -import { RowComponent, ColComponent, WidgetStatAComponent, TemplateIdDirective, ThemeDirective, DropdownComponent, ButtonDirective, DropdownToggleDirective, DropdownMenuDirective, DropdownItemDirective, DropdownDividerDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-widgets-dropdown', - templateUrl: './widgets-dropdown.component.html', - styleUrls: ['./widgets-dropdown.component.scss'], - changeDetection: ChangeDetectionStrategy.Default, - standalone: true, - imports: [RowComponent, ColComponent, WidgetStatAComponent, TemplateIdDirective, IconDirective, ThemeDirective, DropdownComponent, ButtonDirective, DropdownToggleDirective, DropdownMenuDirective, DropdownItemDirective, RouterLink, DropdownDividerDirective, ChartjsComponent] -}) -export class WidgetsDropdownComponent implements OnInit, AfterContentInit { - - constructor( - private changeDetectorRef: ChangeDetectorRef - ) {} - - data: any[] = []; - options: any[] = []; - labels = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December', - 'January', - 'February', - 'March', - 'April' - ]; - datasets = [ - [{ - label: 'My First dataset', - backgroundColor: 'transparent', - borderColor: 'rgba(255,255,255,.55)', - pointBackgroundColor: getStyle('--cui-primary'), - pointHoverBorderColor: getStyle('--cui-primary'), - data: [65, 59, 84, 84, 51, 55, 40] - }], [{ - label: 'My Second dataset', - backgroundColor: 'transparent', - borderColor: 'rgba(255,255,255,.55)', - pointBackgroundColor: getStyle('--cui-info'), - pointHoverBorderColor: getStyle('--cui-info'), - data: [1, 18, 9, 17, 34, 22, 11] - }], [{ - label: 'My Third dataset', - backgroundColor: 'rgba(255,255,255,.2)', - borderColor: 'rgba(255,255,255,.55)', - pointBackgroundColor: getStyle('--cui-warning'), - pointHoverBorderColor: getStyle('--cui-warning'), - data: [78, 81, 80, 45, 34, 12, 40], - fill: true - }], [{ - label: 'My Fourth dataset', - backgroundColor: 'rgba(255,255,255,.2)', - borderColor: 'rgba(255,255,255,.55)', - data: [78, 81, 80, 45, 34, 12, 40, 85, 65, 23, 12, 98, 34, 84, 67, 82], - barPercentage: 0.7 - }] - ]; - optionsDefault = { - plugins: { - legend: { - display: false - } - }, - maintainAspectRatio: false, - scales: { - x: { - border: { - display: false, - }, - grid: { - display: false, - drawBorder: false - }, - ticks: { - display: false - } - }, - y: { - min: 30, - max: 89, - display: false, - grid: { - display: false - }, - ticks: { - display: false - } - } - }, - elements: { - line: { - borderWidth: 1, - tension: 0.4 - }, - point: { - radius: 4, - hitRadius: 10, - hoverRadius: 4 - } - } - }; - - ngOnInit(): void { - this.setData(); - } - - ngAfterContentInit(): void { - this.changeDetectorRef.detectChanges(); - - } - - setData() { - for (let idx = 0; idx < 4; idx++) { - this.data[idx] = { - labels: idx < 3 ? this.labels.slice(0, 7) : this.labels, - datasets: this.datasets[idx] - }; - } - this.setOptions(); - } - - setOptions() { - for (let idx = 0; idx < 4; idx++) { - const options = JSON.parse(JSON.stringify(this.optionsDefault)); - switch (idx) { - case 0: { - this.options.push(options); - break; - } - case 1: { - options.scales.y.min = -9; - options.scales.y.max = 39; - options.elements.line.tension = 0; - this.options.push(options); - break; - } - case 2: { - options.scales.x = { display: false }; - options.scales.y = { display: false }; - options.elements.line.borderWidth = 2; - options.elements.point.radius = 0; - this.options.push(options); - break; - } - case 3: { - options.scales.x.grid = { display: false, drawTicks: false }; - options.scales.x.grid = { display: false, drawTicks: false, drawBorder: false }; - options.scales.y.min = undefined; - options.scales.y.max = undefined; - options.elements = {}; - this.options.push(options); - break; - } - } - } - } -} - -@Component({ - selector: 'app-chart-sample', - template: '', - standalone: true, - imports: [ChartjsComponent] -}) -export class ChartSample implements AfterViewInit { - - constructor() {} - - @ViewChild('chart') chartComponent!: ChartjsComponent; - - colors = { - label: 'My dataset', - backgroundColor: 'rgba(77,189,116,.2)', - borderColor: '#4dbd74', - pointHoverBackgroundColor: '#fff' - }; - - labels = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']; - - data = { - labels: this.labels, - datasets: [{ - data: [65, 59, 84, 84, 51, 55, 40], - ...this.colors, - fill: { value: 65 } - }] - }; - - options = { - maintainAspectRatio: false, - plugins: { - legend: { - display: false - } - }, - elements: { - line: { - tension: 0.4 - } - } - }; - - ngAfterViewInit(): void { - setTimeout(() => { - const data = () => { - return { - ...this.data, - labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'], - datasets: [{ - ...this.data.datasets[0], - data: [42, 88, 42, 66, 77], - fill: { value: 55 } - }, { ...this.data.datasets[0], borderColor: '#ffbd47', data: [88, 42, 66, 77, 42] }] - }; - }; - const newLabels = ['Jan', 'Feb', 'Mar', 'Apr', 'May']; - const newData = [42, 88, 42, 66, 77]; - let { datasets, labels } = { ...this.data }; - // @ts-ignore - const before = this.chartComponent?.chart?.data.datasets.length; - console.log('before', before); - // console.log('datasets, labels', datasets, labels) - // @ts-ignore - // this.data = data() - this.data = { - ...this.data, - datasets: [{ ...this.data.datasets[0], data: newData }, { - ...this.data.datasets[0], - borderColor: '#ffbd47', - data: [88, 42, 66, 77, 42] - }], - labels: newLabels - }; - // console.log('datasets, labels', { datasets, labels } = {...this.data}) - // @ts-ignore - setTimeout(() => { - const after = this.chartComponent?.chart?.data.datasets.length; - console.log('after', after); - }); - }, 5000); - } -} diff --git a/src/app/views/widgets/widgets-e/widgets-e.component.html b/src/app/views/widgets/widgets-e/widgets-e.component.html deleted file mode 100644 index 1df9a262b..000000000 --- a/src/app/views/widgets/widgets-e/widgets-e.component.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/app/views/widgets/widgets-e/widgets-e.component.scss b/src/app/views/widgets/widgets-e/widgets-e.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/widgets/widgets-e/widgets-e.component.spec.ts b/src/app/views/widgets/widgets-e/widgets-e.component.spec.ts deleted file mode 100644 index 55e8cded3..000000000 --- a/src/app/views/widgets/widgets-e/widgets-e.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { GridModule, WidgetModule } from '@coreui/angular'; -import { ChartjsModule } from '@coreui/angular-chartjs'; -import { IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { WidgetsEComponent } from './widgets-e.component'; - -describe('WidgetsEComponent', () => { - let component: WidgetsEComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [WidgetModule, GridModule, ChartjsModule, WidgetsEComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(WidgetsEComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/widgets/widgets-e/widgets-e.component.ts b/src/app/views/widgets/widgets-e/widgets-e.component.ts deleted file mode 100644 index a21806250..000000000 --- a/src/app/views/widgets/widgets-e/widgets-e.component.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core'; -import { getStyle } from '@coreui/utils'; -import { ChartjsComponent } from '@coreui/angular-chartjs'; -import { RowComponent, ColComponent, WidgetStatEComponent, TextColorDirective } from '@coreui/angular'; - -@Component({ - selector: 'app-widgets-e', - templateUrl: './widgets-e.component.html', - styleUrls: ['./widgets-e.component.scss'], - changeDetection: ChangeDetectionStrategy.Default, - standalone: true, - imports: [RowComponent, ColComponent, TextColorDirective, WidgetStatEComponent, ChartjsComponent] -}) -export class WidgetsEComponent implements AfterContentInit { - - constructor( - private changeDetectorRef: ChangeDetectorRef - ) { - this.prepareLabels(); - this.prepareDatasets(); - this.prepareData(); - } - - datasets: any[] = []; - labels: string[] = []; - data: any[] = []; - barOptions = { - maintainAspectRatio: false, - plugins: { - legend: { - display: false - } - }, - scales: { - x: { - display: false - }, - y: { - display: false - } - } - }; - lineOptions = { - maintainAspectRatio: false, - elements: { - line: { - tension: 0.4 - }, - point: { - radius: 0 - } - }, - plugins: { - legend: { - display: false - } - }, - scales: { - x: { - display: false - }, - y: { - display: false - } - } - }; - - get random() { - const min = 40, - max = 100; - return Math.floor(Math.random() * (max - min + 1) + min); - } - - get randomData() { - const data = []; - for (let i = 0; i < 15; i++) { - data.push(this.random); - } - return data; - } - - get baseDatasets(): Array { - return [ - { - data: this.randomData, - barThickness: 'flex', - borderColor: 'transparent', - backgroundColor: 'transparent', - pointBackgroundColor: 'transparent', - pointHoverBorderColor: 'transparent', - borderWidth: 1 - } - ]; - } - - ngAfterContentInit(): void { - this.changeDetectorRef.detectChanges(); - } - - prepareData() { - for (let i = 0; i < 6; i++) { - this.data.push({ labels: this.labels, datasets: this.datasets[i] }); - } - } - - prepareLabels() { - for (let i = 0; i < 15; i++) { - this.labels.push(this.getDayName(i)); - } - } - - prepareDatasets() { - const params = [ - { backgroundColor: 'danger' }, - { backgroundColor: 'primary' }, - { backgroundColor: 'secondary' }, - { borderColor: 'danger', borderWidth: 2 }, - { borderColor: 'success', borderWidth: 2 }, - { borderColor: 'info', borderWidth: 2 } - ]; - for (let i = 0; i < 6; i++) { - this.datasets.push(this.getDataset(params[i])); - } - } - - getDataset({ backgroundColor = 'transparent', borderColor = 'transparent', borderWidth = 1 }) { - const dataset = this.baseDatasets; - dataset[0].backgroundColor = backgroundColor !== 'transparent' ? getStyle(`--cui-${backgroundColor}`) : backgroundColor; - dataset[0].borderColor = borderColor !== 'transparent' ? getStyle(`--cui-${borderColor}`) : borderColor; - dataset[0].pointBackgroundColor = getStyle(`--cui-${borderColor}`); - dataset[0].borderWidth = borderWidth; - return dataset; - } - - getDayName(shift = 0) { - // @ts-ignore - const locale = navigator.language ?? navigator.userLanguage ?? navigator.systemLanguage ?? navigator.browserLanguage ?? 'en-US'; - const baseDate = new Date(Date.UTC(2000, 1, 0)); // Monday - baseDate.setDate(baseDate.getDate() + shift); - return baseDate.toLocaleDateString(locale, { weekday: 'short' }); - } -} diff --git a/src/app/views/widgets/widgets/widgets.component.html b/src/app/views/widgets/widgets/widgets.component.html deleted file mode 100644 index bb71ea50d..000000000 --- a/src/app/views/widgets/widgets/widgets.component.html +++ /dev/null @@ -1,534 +0,0 @@ - - Widgets - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - View more - - - - - - - - - - - - - View more - - - - - - - - - - - - - View more - - - - - - - - - - - - - View more - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/app/views/widgets/widgets/widgets.component.scss b/src/app/views/widgets/widgets/widgets.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/views/widgets/widgets/widgets.component.spec.ts b/src/app/views/widgets/widgets/widgets.component.spec.ts deleted file mode 100644 index a60ecf8d7..000000000 --- a/src/app/views/widgets/widgets/widgets.component.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { CardModule, GridModule, ProgressModule, WidgetModule } from '@coreui/angular'; -import { ChartjsModule } from '@coreui/angular-chartjs'; -import { IconModule, IconSetService } from '@coreui/icons-angular'; -import { iconSubset } from '../../../icons/icon-subset'; -import { WidgetsBrandComponent } from '../widgets-brand/widgets-brand.component'; -import { WidgetsDropdownComponent } from '../widgets-dropdown/widgets-dropdown.component'; -import { WidgetsEComponent } from '../widgets-e/widgets-e.component'; -import { WidgetsComponent } from './widgets.component'; - -describe('WidgetsComponent', () => { - let component: WidgetsComponent; - let fixture: ComponentFixture; - let iconSetService: IconSetService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [WidgetModule, ProgressModule, GridModule, CardModule, RouterTestingModule, ChartjsModule, IconModule, WidgetsComponent, WidgetsBrandComponent, WidgetsDropdownComponent, WidgetsEComponent], - providers: [IconSetService] -}) - .compileComponents(); - }); - - beforeEach(() => { - iconSetService = TestBed.inject(IconSetService); - iconSetService.icons = { ...iconSubset }; - - fixture = TestBed.createComponent(WidgetsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/views/widgets/widgets/widgets.component.ts b/src/app/views/widgets/widgets/widgets.component.ts deleted file mode 100644 index d7f44241c..000000000 --- a/src/app/views/widgets/widgets/widgets.component.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core'; -import { WidgetsBrandComponent } from '../widgets-brand/widgets-brand.component'; -import { IconDirective } from '@coreui/icons-angular'; -import { WidgetsEComponent } from '../widgets-e/widgets-e.component'; -import { WidgetsDropdownComponent } from '../widgets-dropdown/widgets-dropdown.component'; -import { DocsExampleComponent } from '@docs-components/public-api'; -import { TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, RowComponent, ColComponent, WidgetStatBComponent, ProgressBarDirective, ProgressComponent, WidgetStatFComponent, TemplateIdDirective, CardGroupComponent, WidgetStatCComponent } from '@coreui/angular'; - -@Component({ - selector: 'app-widgets', - templateUrl: './widgets.component.html', - styleUrls: ['./widgets.component.scss'], - changeDetection: ChangeDetectionStrategy.Default, - standalone: true, - imports: [TextColorDirective, CardComponent, CardHeaderComponent, CardBodyComponent, DocsExampleComponent, WidgetsDropdownComponent, RowComponent, ColComponent, WidgetStatBComponent, ProgressBarDirective, ProgressComponent, WidgetsEComponent, WidgetStatFComponent, TemplateIdDirective, IconDirective, WidgetsBrandComponent, CardGroupComponent, WidgetStatCComponent] -}) -export class WidgetsComponent implements AfterContentInit { - constructor( - private changeDetectorRef: ChangeDetectorRef - ) {} - - ngAfterContentInit(): void { - this.changeDetectorRef.detectChanges(); - } -} diff --git a/src/assets/images/avatars/1.jpg b/src/assets/images/avatars/1.jpg deleted file mode 100644 index 8b5f8091c..000000000 Binary files a/src/assets/images/avatars/1.jpg and /dev/null differ diff --git a/src/assets/images/avatars/2.jpg b/src/assets/images/avatars/2.jpg deleted file mode 100644 index 161eeef53..000000000 Binary files a/src/assets/images/avatars/2.jpg and /dev/null differ diff --git a/src/assets/images/avatars/3.jpg b/src/assets/images/avatars/3.jpg deleted file mode 100644 index 53ecc542f..000000000 Binary files a/src/assets/images/avatars/3.jpg and /dev/null differ diff --git a/src/assets/images/avatars/4.jpg b/src/assets/images/avatars/4.jpg deleted file mode 100644 index a6ee3c72e..000000000 Binary files a/src/assets/images/avatars/4.jpg and /dev/null differ diff --git a/src/assets/images/avatars/5.jpg b/src/assets/images/avatars/5.jpg deleted file mode 100644 index c38baeed7..000000000 Binary files a/src/assets/images/avatars/5.jpg and /dev/null differ diff --git a/src/assets/images/avatars/6.jpg b/src/assets/images/avatars/6.jpg deleted file mode 100644 index 57bbf9685..000000000 Binary files a/src/assets/images/avatars/6.jpg and /dev/null differ diff --git a/src/assets/images/avatars/7.jpg b/src/assets/images/avatars/7.jpg deleted file mode 100644 index dfc20b7a7..000000000 Binary files a/src/assets/images/avatars/7.jpg and /dev/null differ diff --git a/src/assets/images/avatars/8.jpg b/src/assets/images/avatars/8.jpg deleted file mode 100644 index 4e8b48d4f..000000000 Binary files a/src/assets/images/avatars/8.jpg and /dev/null differ diff --git a/src/assets/images/avatars/9.jpg b/src/assets/images/avatars/9.jpg deleted file mode 100644 index f690e78cc..000000000 Binary files a/src/assets/images/avatars/9.jpg and /dev/null differ diff --git a/src/index.html b/src/index.html deleted file mode 100644 index e9f4e9c6e..000000000 --- a/src/index.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - CoreUI Free Angular Admin Template - - - - -
- - Loading... -
-
- - diff --git a/src/scss/_custom.scss b/src/scss/_custom.scss deleted file mode 100644 index 62ae7cc14..000000000 --- a/src/scss/_custom.scss +++ /dev/null @@ -1,17 +0,0 @@ -// Here you can add other styles - -// custom .chartjs-tooltip-body-item padding -@import "charts"; - -// custom tweaks for scrollbar styling (wip) -@import "scrollbar"; - - // custom calendar today cell color -.calendar-cell.today { - --cui-calendar-cell-today-color: var(--cui-info) !important; -} - -// custom select week cursor pointer -.select-week .calendar-row.current { - cursor: pointer; -} diff --git a/src/scss/styles.scss b/src/scss/styles.scss deleted file mode 100644 index 27a0a97c0..000000000 --- a/src/scss/styles.scss +++ /dev/null @@ -1,23 +0,0 @@ -/* You can add global styles to this file, and also import other style files */ - -// If you want to override variables do it here -@import "variables"; - -// Import styles with default layout. -@import "@coreui/coreui/scss/coreui"; - -// Import Chart.js custom tooltips styles -@import "@coreui/chartjs/scss/coreui-chartjs"; - -// Custom styles for this theme -@import "theme"; - -// Some temp fixes -//@import "fixes"; - -// If you want to add custom CSS you can put it here. -@import "custom"; - -// Examples -// We use those styles to show code examples, you should remove them in your application. -@import "examples"; diff --git a/tsconfig.app.json b/tsconfig.app.json deleted file mode 100644 index 9d40aa3d5..000000000 --- a/tsconfig.app.json +++ /dev/null @@ -1,21 +0,0 @@ -/* To learn more about this file see: https://angular.io/config/tsconfig. */ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./out-tsc/app", - "types": [ - "@angular/localize" - ], - "paths": { - "@docs-components/*": [ - "./src/components/*" - ] - } - }, - "files": [ - "src/main.ts" - ], - "include": [ - "src/**/*.d.ts" - ] -}