diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2676bb9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +node_modules +dist +.git +.gitignore +.env +.env.local +.env.production +.env.development +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.DS_Store +.vscode +.idea +README.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2fe8f68 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# Backend API Configuration +VITE_BACKEND_API_URL=http://localhost:3000/api/v1 + +# Environment +VITE_ENV=development + +# Optional: Analytics +# VITE_ANALYTICS_ID= + +# Optional: Sentry Error Tracking +# VITE_SENTRY_DSN= diff --git a/.env.production.example b/.env.production.example new file mode 100644 index 0000000..1e22db1 --- /dev/null +++ b/.env.production.example @@ -0,0 +1,11 @@ +# Backend API Configuration +VITE_BACKEND_API_URL=https://api.yourdomain.com/api/v1 + +# Environment +VITE_ENV=production + +# Optional: Analytics +# VITE_ANALYTICS_ID=your-analytics-id + +# Optional: Sentry Error Tracking +# VITE_SENTRY_DSN=your-sentry-dsn diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..293c5d0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,85 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + test: + name: Test & Build + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run linter + run: npm run lint + + - name: Run type check + run: npm run type-check + + - name: Run tests + run: npm run test:run + + - name: Run tests with coverage + run: npm run test:coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage/coverage-final.json + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + + - name: Build application + run: npm run build + env: + VITE_BACKEND_API_URL: ${{ secrets.VITE_BACKEND_API_URL }} + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist-${{ matrix.node-version }} + path: dist/ + retention-days: 7 + + security: + name: Security Audit + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Run npm audit + run: npm audit --audit-level=moderate + continue-on-error: true + + - name: Run Snyk security scan + uses: snyk/actions/node@master + continue-on-error: true + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..aafb24a --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,52 @@ +name: Build Production + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + build: + name: Build Production Artifacts + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm run test:run + + - name: Build for production + run: npm run build:prod + env: + VITE_BACKEND_API_URL: ${{ secrets.VITE_BACKEND_API_URL_PROD }} + VITE_ENV: production + VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }} + + - name: Upload production artifacts + uses: actions/upload-artifact@v4 + with: + name: production-build + path: dist/ + retention-days: 30 + + - name: Build Docker image + run: docker build -t yaltopia-admin:${{ github.sha }} . + + - name: Build success notification + if: success() + run: echo "Production build successful!" + + - name: Build failure notification + if: failure() + run: echo "Production build failed!" diff --git a/.gitignore b/.gitignore index a547bf3..f66b568 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,15 @@ lerna-debug.log* node_modules dist dist-ssr +coverage *.local +# Environment variables +.env +.env.local +.env.production +.env.development + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bd8c901 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# Build stage +FROM node:18-alpine as build + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy source code +COPY . . + +# Build the application +RUN npm run build:prod + +# Production stage +FROM nginx:alpine + +# Copy built assets from build stage +COPY --from=build /app/dist /usr/share/nginx/html + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Expose port 80 +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index d2e7761..9d1b26c 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,233 @@ -# React + TypeScript + Vite +# Yaltopia Ticket Admin -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +Admin dashboard for Yaltopia Ticket management system built with React, TypeScript, and Vite. -Currently, two official plugins are available: +> 📚 **For detailed documentation, see [dev-docs/](./dev-docs/README.md)** -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +## Features -## React Compiler +- User Management +- Analytics Dashboard +- Security Monitoring +- System Health Monitoring +- Audit Logs +- Announcements Management +- Maintenance Mode +- API Key Management -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). +## Tech Stack -## Expanding the ESLint configuration +- React 19 +- TypeScript +- Vite +- TanStack Query (React Query) +- React Router v7 +- Tailwind CSS +- Radix UI Components +- Recharts for data visualization +- Axios for API calls -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: +## Prerequisites -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... +- Node.js 18+ +- npm or yarn - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, +## Getting Started - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) +### 1. Clone the repository + +```bash +git clone +cd yaltopia-ticket-admin ``` -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: +### 2. Install dependencies -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) +```bash +npm install ``` + +### 3. Environment Configuration + +Copy the example environment file and configure it: + +```bash +cp .env.example .env +``` + +Edit `.env` and set your API URL: + +```env +VITE_BACKEND_API_URL=http://localhost:3000/api/v1 +VITE_ENV=development +``` + +### 4. Run development server + +```bash +npm run dev +``` + +The application will be available at `http://localhost:5173` + +## Building for Production + +### 1. Configure production environment + +Copy the production environment example: + +```bash +cp .env.production.example .env.production +``` + +Edit `.env.production` with your production API URL: + +```env +VITE_BACKEND_API_URL=https://api.yourdomain.com/api/v1 +VITE_ENV=production +``` + +### 2. Build the application + +```bash +npm run build:prod +``` + +The production build will be in the `dist` directory. + +### 3. Preview production build locally + +```bash +npm run preview +``` + +## Deployment + +### Docker Deployment (Recommended) + +1. Build the Docker image: +```bash +docker build -t yaltopia-admin:latest . +``` + +2. Run the container: +```bash +docker run -p 8080:80 yaltopia-admin:latest +``` + +3. Deploy to your cloud provider (AWS, GCP, Azure, DigitalOcean, etc.) + +### Traditional VPS Deployment + +1. Build the application: `npm run build:prod` +2. Copy the `dist` directory to your web server +3. Configure nginx or Apache to serve the static files +4. Set up redirects for SPA routing (see nginx.conf example) + +### SPA Routing Configuration + +For proper routing with nginx, use the included `nginx.conf` file or add this configuration: + +```nginx +location / { + try_files $uri $uri/ /index.html; +} +``` +The project includes a `Dockerfile` and `nginx.conf` for containerized deployment. + +See [DEPLOYMENT.md](dev-docs/DEPLOYMENT.md) for detailed deployment instructions. + + location / { + try_files $uri $uri/ /index.html; + } + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; +} +``` + +Build and run: + +```bash +docker build -t yaltopia-admin . +docker run -p 80:80 yaltopia-admin +``` + +## Environment Variables + +| Variable | Description | Required | Default | +|----------|-------------|----------|---------| +| `VITE_BACKEND_API_URL` | Backend API base URL | Yes | `http://localhost:3000/api/v1` | +| `VITE_ENV` | Environment name | No | `development` | +| `VITE_ANALYTICS_ID` | Analytics tracking ID | No | - | +| `VITE_SENTRY_DSN` | Sentry error tracking DSN | No | - | + +## Scripts + +- `npm run dev` - Start development server +- `npm run build` - Build for production +- `npm run build:prod` - Build with production environment +- `npm run preview` - Preview production build +- `npm run lint` - Run ESLint +- `npm run lint:fix` - Fix ESLint errors +- `npm run type-check` - Run TypeScript type checking + +## Project Structure + +``` +src/ +├── app/ # App configuration (query client) +├── assets/ # Static assets +├── components/ # Reusable UI components +│ └── ui/ # Shadcn UI components +├── layouts/ # Layout components +├── lib/ # Utilities and API client +├── pages/ # Page components +│ ├── admin/ # Admin pages +│ ├── dashboard/ # Dashboard pages +│ └── ... +├── App.tsx # Main app component +├── main.tsx # App entry point +└── index.css # Global styles +``` + +## Security Considerations + +### Current Implementation + +- JWT tokens stored in localStorage +- Token automatically attached to API requests +- Automatic redirect to login on 401 errors +- Error handling for common HTTP status codes + +### Production Recommendations + +1. **Use httpOnly cookies** instead of localStorage for tokens +2. **Implement HTTPS** - Never deploy without SSL/TLS +3. **Add security headers** - CSP, HSTS, X-Frame-Options +4. **Enable CORS** properly on your backend +5. **Implement rate limiting** on authentication endpoints +6. **Add error boundary** for graceful error handling +7. **Set up monitoring** (Sentry, LogRocket, etc.) +8. **Regular security audits** - Run `npm audit` regularly + +## Browser Support + +- Chrome (latest) +- Firefox (latest) +- Safari (latest) +- Edge (latest) + +## Contributing + +1. Create a feature branch +2. Make your changes +3. Run linting and type checking +4. Submit a pull request + +## License + +Proprietary - All rights reserved + diff --git a/dev-docs/API_GUIDE.md b/dev-docs/API_GUIDE.md new file mode 100644 index 0000000..e446186 --- /dev/null +++ b/dev-docs/API_GUIDE.md @@ -0,0 +1,983 @@ +# API & Service Layer Guide + +Complete guide for making API calls in the Yaltopia Ticket Admin application. + +## Table of Contents + +- [Architecture Overview](#architecture-overview) +- [Available Services](#available-services) +- [Basic Usage](#basic-usage) +- [Common Patterns](#common-patterns) +- [Service Methods Reference](#service-methods-reference) +- [Error Handling](#error-handling) +- [Best Practices](#best-practices) +- [Examples](#examples) + +--- + +## Architecture Overview + +### The Service Layer Pattern + +All API calls flow through a centralized service layer: + +``` +┌─────────────────┐ +│ Component │ "I need user data" +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ Service Layer │ userService.getUsers() +│ (Business │ • Type-safe methods +│ Logic) │ • Error handling +└────────┬────────┘ • Data transformation + │ + ▼ +┌─────────────────┐ +│ API Client │ axios.get('/admin/users') +│ (HTTP Layer) │ • Auth token injection +└────────┬────────┘ • Token refresh + │ • Request/response interceptors + ▼ +┌─────────────────┐ +│ Backend API │ Returns JSON data +└─────────────────┘ +``` + +### Why Service Layer? + +**Before (Bad):** +```typescript +// Direct API calls - scattered everywhere +const response = await axios.get('/api/users') +const users = response.data + +// Different patterns in different files +fetch('/api/users').then(r => r.json()) +``` + +**After (Good):** +```typescript +// Centralized, typed, consistent +import { userService } from '@/services' +const users = await userService.getUsers() +``` + +**Benefits:** +- ✅ Single source of truth +- ✅ Type safety (TypeScript) +- ✅ Automatic authentication +- ✅ Consistent error handling +- ✅ Easy to test +- ✅ Easy to maintain + +--- + +## Available Services + +### Import All Services + +```typescript +import { + authService, // Authentication & authorization + userService, // User management + analyticsService, // Dashboard analytics & metrics + securityService, // Security monitoring & logs + systemService, // System health & maintenance + announcementService,// Announcements management + auditService, // Audit logs & history + settingsService // System settings +} from '@/services' +``` + +### Service Locations + +``` +src/services/ +├── index.ts # Central export (import from here) +├── api/ +│ └── client.ts # Shared axios instance +├── auth.service.ts # authService +├── user.service.ts # userService +├── analytics.service.ts # analyticsService +├── security.service.ts # securityService +├── system.service.ts # systemService +├── announcement.service.ts # announcementService +├── audit.service.ts # auditService +└── settings.service.ts # settingsService +``` + +--- + +## Basic Usage + +### 1. Simple API Call + +```typescript +import { userService } from '@/services' + +// Async/await +async function loadUsers() { + const users = await userService.getUsers() + console.log(users) // Typed response +} + +// Promise +userService.getUsers() + .then(users => console.log(users)) + .catch(error => console.error(error)) +``` + +### 2. With React Query (Recommended) + +```typescript +import { useQuery } from '@tanstack/react-query' +import { userService } from '@/services' + +function UsersPage() { + const { data, isLoading, error } = useQuery({ + queryKey: ['users'], + queryFn: () => userService.getUsers() + }) + + if (isLoading) return
Loading...
+ if (error) return
Error: {error.message}
+ + return ( +
+ {data?.data.map(user => ( +
{user.email}
+ ))} +
+ ) +} +``` + +### 3. With Parameters + +```typescript +import { userService } from '@/services' + +// Fetch with filters +const users = await userService.getUsers({ + page: 1, + limit: 20, + search: 'john', + role: 'ADMIN', + isActive: true +}) + +// Single user +const user = await userService.getUser('user-id-123') +``` + +### 4. Mutations (Create/Update/Delete) + +```typescript +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { userService } from '@/services' +import { toast } from 'sonner' + +function DeleteUserButton({ userId }) { + const queryClient = useQueryClient() + + const mutation = useMutation({ + mutationFn: () => userService.deleteUser(userId), + onSuccess: () => { + // Refresh the users list + queryClient.invalidateQueries({ queryKey: ['users'] }) + toast.success('User deleted successfully') + }, + onError: (error: any) => { + toast.error(error.response?.data?.message || 'Failed to delete user') + } + }) + + return ( + + ) +} +``` + +--- + +## Common Patterns + +### Pattern 1: Fetching List Data + +```typescript +import { useQuery } from '@tanstack/react-query' +import { userService } from '@/services' +import { useState } from 'react' + +function UsersPage() { + const [page, setPage] = useState(1) + const [search, setSearch] = useState('') + + const { data, isLoading } = useQuery({ + queryKey: ['users', page, search], + queryFn: () => userService.getUsers({ page, limit: 20, search }) + }) + + return ( +
+ setSearch(e.target.value)} + placeholder="Search users..." + /> + + {isLoading ? ( +
Loading...
+ ) : ( +
+ {data?.data.map(user => ( + + ))} + + +
+ )} +
+ ) +} +``` + +### Pattern 2: Creating New Records + +```typescript +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { userService } from '@/services' +import { useState } from 'react' + +function CreateUserForm() { + const queryClient = useQueryClient() + const [formData, setFormData] = useState({ + email: '', + firstName: '', + lastName: '', + role: 'USER' + }) + + const createMutation = useMutation({ + mutationFn: (data) => userService.createUser(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['users'] }) + toast.success('User created successfully') + setFormData({ email: '', firstName: '', lastName: '', role: 'USER' }) + }, + onError: (error: any) => { + toast.error(error.response?.data?.message || 'Failed to create user') + } + }) + + const handleSubmit = (e) => { + e.preventDefault() + createMutation.mutate(formData) + } + + return ( +
+ setFormData({ ...formData, email: e.target.value })} + placeholder="Email" + required + /> + {/* More fields... */} + + +
+ ) +} +``` + +### Pattern 3: Updating Records + +```typescript +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { userService } from '@/services' + +function UpdateUserButton({ userId, updates }) { + const queryClient = useQueryClient() + + const updateMutation = useMutation({ + mutationFn: () => userService.updateUser(userId, updates), + onSuccess: () => { + // Refresh both the list and the single user + queryClient.invalidateQueries({ queryKey: ['users'] }) + queryClient.invalidateQueries({ queryKey: ['users', userId] }) + toast.success('User updated successfully') + } + }) + + return ( + + ) +} +``` + +### Pattern 4: File Upload + +```typescript +import { userService } from '@/services' + +function ImportUsersButton() { + const handleFileUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + + try { + const result = await userService.importUsers(file) + toast.success(`Imported ${result.imported} users. ${result.failed} failed.`) + } catch (error: any) { + toast.error(error.response?.data?.message || 'Import failed') + } + } + + return ( + + ) +} +``` + +### Pattern 5: File Download + +```typescript +import { userService } from '@/services' + +function ExportUsersButton() { + const handleExport = async () => { + try { + const blob = await userService.exportUsers('csv') + + // Create download link + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `users-${new Date().toISOString()}.csv` + a.click() + + window.URL.revokeObjectURL(url) + toast.success('Users exported successfully') + } catch (error: any) { + toast.error('Export failed') + } + } + + return ( + + ) +} +``` + +--- + +## Service Methods Reference + +### AuthService + +```typescript +import { authService } from '@/services' + +// Login +const response = await authService.login({ + email: 'admin@example.com', + password: 'password' +}) +// Returns: { accessToken, refreshToken, user } + +// Logout +await authService.logout() + +// Refresh token +await authService.refreshToken() + +// Get current user (from localStorage) +const user = authService.getCurrentUser() + +// Check if authenticated +const isAuth = authService.isAuthenticated() + +// Check if admin +const isAdmin = authService.isAdmin() +``` + +### UserService + +```typescript +import { userService } from '@/services' + +// Get paginated users +const users = await userService.getUsers({ + page: 1, + limit: 20, + search: 'john', + role: 'ADMIN', + isActive: true +}) + +// Get single user +const user = await userService.getUser('user-id') + +// Create user +const newUser = await userService.createUser({ + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + role: 'USER' +}) + +// Update user +const updated = await userService.updateUser('user-id', { + isActive: false, + role: 'ADMIN' +}) + +// Delete user +await userService.deleteUser('user-id', hard = false) + +// Reset password +const result = await userService.resetPassword('user-id') +// Returns: { temporaryPassword: 'abc123' } + +// Get user activity +const activity = await userService.getUserActivity('user-id', days = 30) + +// Import users from CSV +const result = await userService.importUsers(file) +// Returns: { imported: 10, failed: 2 } + +// Export users to CSV +const blob = await userService.exportUsers('csv') +``` + +### AnalyticsService + +```typescript +import { analyticsService } from '@/services' + +// Get overview statistics +const stats = await analyticsService.getOverview() +// Returns: { totalUsers, activeUsers, totalRevenue, ... } + +// Get user growth data +const growth = await analyticsService.getUserGrowth(days = 30) +// Returns: [{ date: '2024-01-01', users: 100, ... }] + +// Get revenue data +const revenue = await analyticsService.getRevenue('30days') +// Options: '7days', '30days', '90days' + +// Get API usage +const apiUsage = await analyticsService.getApiUsage(days = 7) + +// Get error rate +const errorRate = await analyticsService.getErrorRate(days = 7) + +// Get storage analytics +const storage = await analyticsService.getStorageAnalytics() +``` + +### SecurityService + +```typescript +import { securityService } from '@/services' + +// Get suspicious activity +const suspicious = await securityService.getSuspiciousActivity() +// Returns: { suspiciousIPs: [...], suspiciousEmails: [...] } + +// Get active sessions +const sessions = await securityService.getActiveSessions() + +// Terminate session +await securityService.terminateSession('session-id') + +// Get failed login attempts +const failedLogins = await securityService.getFailedLogins({ + page: 1, + limit: 50, + email: 'user@example.com' +}) + +// Get rate limit violations +const violations = await securityService.getRateLimitViolations(days = 7) + +// Get all API keys +const apiKeys = await securityService.getAllApiKeys() + +// Revoke API key +await securityService.revokeApiKey('key-id') + +// Ban IP address +await securityService.banIpAddress('192.168.1.1', 'Suspicious activity') +``` + +### SystemService + +```typescript +import { systemService } from '@/services' + +// Get system health +const health = await systemService.getHealth() +// Returns: { status: 'healthy', database: 'connected', ... } + +// Get system info +const info = await systemService.getSystemInfo() +// Returns: { version, environment, memory, cpu, ... } + +// Get maintenance status +const maintenance = await systemService.getMaintenanceStatus() + +// Enable maintenance mode +await systemService.enableMaintenance('System upgrade in progress') + +// Disable maintenance mode +await systemService.disableMaintenance() + +// Clear cache +await systemService.clearCache() + +// Run migrations +const result = await systemService.runMigrations() +``` + +### AnnouncementService + +```typescript +import { announcementService } from '@/services' + +// Get all announcements +const announcements = await announcementService.getAnnouncements(activeOnly = false) + +// Get single announcement +const announcement = await announcementService.getAnnouncement('announcement-id') + +// Create announcement +const newAnnouncement = await announcementService.createAnnouncement({ + title: 'Maintenance Notice', + message: 'System will be down for maintenance', + type: 'warning', + priority: 1, + targetAudience: 'all' +}) + +// Update announcement +const updated = await announcementService.updateAnnouncement('announcement-id', { + title: 'Updated Title' +}) + +// Toggle announcement active status +await announcementService.toggleAnnouncement('announcement-id') + +// Delete announcement +await announcementService.deleteAnnouncement('announcement-id') +``` + +### AuditService + +```typescript +import { auditService } from '@/services' + +// Get audit logs +const logs = await auditService.getAuditLogs({ + page: 1, + limit: 50, + userId: 'user-id', + action: 'DELETE', + resourceType: 'user', + startDate: '2024-01-01', + endDate: '2024-12-31' +}) + +// Get audit log by ID +const log = await auditService.getAuditLog('log-id') + +// Get user audit activity +const activity = await auditService.getUserAuditActivity('user-id', days = 30) + +// Get resource history +const history = await auditService.getResourceHistory('user', 'resource-id') + +// Get audit statistics +const stats = await auditService.getAuditStats(startDate, endDate) + +// Export audit logs +const blob = await auditService.exportAuditLogs({ + format: 'csv', + startDate: '2024-01-01', + endDate: '2024-12-31' +}) +``` + +### SettingsService + +```typescript +import { settingsService } from '@/services' + +// Get all settings +const settings = await settingsService.getSettings(category = 'GENERAL') + +// Get single setting +const setting = await settingsService.getSetting('feature_flag') + +// Create setting +const newSetting = await settingsService.createSetting({ + key: 'feature_flag', + value: 'true', + category: 'FEATURES', + description: 'Enable new feature', + isPublic: false +}) + +// Update setting +const updated = await settingsService.updateSetting('feature_flag', { + value: 'false' +}) + +// Delete setting +await settingsService.deleteSetting('feature_flag') + +// Get public settings (for frontend use) +const publicSettings = await settingsService.getPublicSettings() +``` + +--- + +## Error Handling + +### Standard Error Pattern + +All services throw errors with consistent structure: + +```typescript +try { + await userService.deleteUser(userId) + toast.success('User deleted') +} catch (error: any) { + // Error structure: + // error.response.status - HTTP status code + // error.response.data.message - Error message from backend + + const message = error.response?.data?.message || 'Operation failed' + toast.error(message) +} +``` + +### Common HTTP Status Codes + +```typescript +// 400 - Bad Request (validation error) +catch (error: any) { + if (error.response?.status === 400) { + toast.error('Invalid input: ' + error.response.data.message) + } +} + +// 401 - Unauthorized (handled automatically by interceptor) +// Token refresh attempted automatically +// If refresh fails, user redirected to login + +// 403 - Forbidden (no permission) +catch (error: any) { + if (error.response?.status === 403) { + toast.error('You do not have permission to perform this action') + } +} + +// 404 - Not Found +catch (error: any) { + if (error.response?.status === 404) { + toast.error('Resource not found') + } +} + +// 500 - Server Error +catch (error: any) { + if (error.response?.status === 500) { + toast.error('Server error. Please try again later.') + } +} +``` + +### React Query Error Handling + +```typescript +const { data, error } = useQuery({ + queryKey: ['users'], + queryFn: () => userService.getUsers(), + retry: 3, // Retry failed requests + retryDelay: 1000, // Wait 1s between retries +}) + +// Display error +if (error) { + return
Error: {error.message}
+} +``` + +--- + +## Best Practices + +### ✅ DO: Use Services + +```typescript +// Good +import { userService } from '@/services' +const users = await userService.getUsers() +``` + +### ❌ DON'T: Direct API Calls + +```typescript +// Bad - don't do this +import axios from 'axios' +const response = await axios.get('/api/users') +``` + +### ✅ DO: Use React Query + +```typescript +// Good - caching, loading states, error handling +const { data, isLoading, error } = useQuery({ + queryKey: ['users'], + queryFn: () => userService.getUsers() +}) +``` + +### ❌ DON'T: Manual State Management + +```typescript +// Bad - manual state management +const [users, setUsers] = useState([]) +const [loading, setLoading] = useState(false) + +useEffect(() => { + setLoading(true) + userService.getUsers() + .then(setUsers) + .finally(() => setLoading(false)) +}, []) +``` + +### ✅ DO: Specific Query Keys + +```typescript +// Good - specific, cacheable +queryKey: ['users', page, limit, search] +``` + +### ❌ DON'T: Generic Query Keys + +```typescript +// Bad - too generic +queryKey: ['data'] +``` + +### ✅ DO: Invalidate After Mutations + +```typescript +// Good - refresh data after changes +const mutation = useMutation({ + mutationFn: userService.deleteUser, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['users'] }) + } +}) +``` + +### ✅ DO: Handle Errors + +```typescript +// Good - user feedback +try { + await userService.deleteUser(id) + toast.success('User deleted') +} catch (error: any) { + toast.error(error.response?.data?.message) +} +``` + +### ❌ DON'T: Ignore Errors + +```typescript +// Bad - no error handling +await userService.deleteUser(id) +``` + +--- + +## Examples + +### Complete CRUD Example + +```typescript +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { userService } from '@/services' +import { toast } from 'sonner' +import { useState } from 'react' + +function UsersManagement() { + const queryClient = useQueryClient() + const [page, setPage] = useState(1) + const [editingUser, setEditingUser] = useState(null) + + // READ - Fetch users + const { data: users, isLoading } = useQuery({ + queryKey: ['users', page], + queryFn: () => userService.getUsers({ page, limit: 20 }) + }) + + // CREATE - Add new user + const createMutation = useMutation({ + mutationFn: (data) => userService.createUser(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['users'] }) + toast.success('User created') + } + }) + + // UPDATE - Edit user + const updateMutation = useMutation({ + mutationFn: ({ id, data }) => userService.updateUser(id, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['users'] }) + setEditingUser(null) + toast.success('User updated') + } + }) + + // DELETE - Remove user + const deleteMutation = useMutation({ + mutationFn: (id) => userService.deleteUser(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['users'] }) + toast.success('User deleted') + } + }) + + if (isLoading) return
Loading...
+ + return ( +
+

Users Management

+ + {/* User List */} + + + + + + + + + + + {users?.data.map(user => ( + + + + + + + ))} + +
EmailNameRoleActions
{user.email}{user.firstName} {user.lastName}{user.role} + + +
+ + {/* Pagination */} +
+ + Page {page} of {users?.totalPages} + +
+
+ ) +} +``` + +--- + +## Summary + +### Key Takeaways + +1. **Always use services** - Never make direct API calls +2. **Use React Query** - For data fetching and caching +3. **Handle errors** - Provide user feedback +4. **Invalidate queries** - After mutations +5. **Use specific query keys** - For better caching + +### Quick Reference + +```typescript +// Import +import { userService } from '@/services' + +// Fetch +const { data } = useQuery({ + queryKey: ['users'], + queryFn: () => userService.getUsers() +}) + +// Mutate +const mutation = useMutation({ + mutationFn: (id) => userService.deleteUser(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['users'] }) + toast.success('Success') + } +}) + +// Error handling +try { + await userService.deleteUser(id) +} catch (error: any) { + toast.error(error.response?.data?.message) +} +``` + +--- + +**For more information:** +- [Development Guide](./DEVELOPMENT.md) - General development practices +- [Testing Guide](./TESTING_GUIDE.md) - How to test services +- [Security Guide](./SECURITY.md) - Security best practices diff --git a/dev-docs/DEPLOYMENT.md b/dev-docs/DEPLOYMENT.md new file mode 100644 index 0000000..5b4f94c --- /dev/null +++ b/dev-docs/DEPLOYMENT.md @@ -0,0 +1,255 @@ +# Deployment Guide + +## Pre-Deployment Checklist + +### 1. Code Quality +- [ ] All TypeScript errors resolved +- [ ] ESLint warnings addressed +- [ ] Build completes successfully +- [ ] No console errors in production build + +### 2. Environment Configuration +- [ ] `.env.production` configured with production API URL +- [ ] All required environment variables set +- [ ] API endpoints tested and accessible +- [ ] CORS configured on backend for production domain + +### 3. Security +- [ ] HTTPS enabled (SSL/TLS certificate) +- [ ] Security headers configured (CSP, HSTS, X-Frame-Options) +- [ ] Authentication tokens secured (consider httpOnly cookies) +- [ ] API keys and secrets not exposed in client code +- [ ] Rate limiting configured on backend +- [ ] Input validation on all forms + +### 4. Performance +- [ ] Code splitting implemented (check vite.config.ts) +- [ ] Images optimized +- [ ] Lazy loading for routes (if needed) +- [ ] Bundle size analyzed and optimized +- [ ] CDN configured for static assets (optional) + +### 5. Monitoring & Error Tracking +- [ ] Error boundary implemented ✓ +- [ ] Error tracking service configured (Sentry, LogRocket, etc.) +- [ ] Analytics configured (Google Analytics, Plausible, etc.) +- [ ] Logging strategy defined +- [ ] Uptime monitoring configured + +### 6. Testing +- [ ] Manual testing completed on staging +- [ ] Cross-browser testing (Chrome, Firefox, Safari, Edge) +- [ ] Mobile responsiveness verified +- [ ] Authentication flow tested +- [ ] API error handling tested + +### 7. Documentation +- [ ] README.md updated ✓ +- [ ] Environment variables documented ✓ +- [ ] Deployment instructions clear ✓ +- [ ] API documentation available + +## Deployment Options + +### Option 1: Docker + Cloud Provider (Recommended) + +1. Build Docker image: +```bash +docker build -t yaltopia-admin:latest . +``` + +2. Test locally: +```bash +docker run -p 8080:80 yaltopia-admin:latest +``` + +3. Push to container registry: +```bash +# For Docker Hub +docker tag yaltopia-admin:latest username/yaltopia-admin:latest +docker push username/yaltopia-admin:latest + +# For AWS ECR +aws ecr get-login-password --region region | docker login --username AWS --password-stdin account-id.dkr.ecr.region.amazonaws.com +docker tag yaltopia-admin:latest account-id.dkr.ecr.region.amazonaws.com/yaltopia-admin:latest +docker push account-id.dkr.ecr.region.amazonaws.com/yaltopia-admin:latest +``` + +4. Deploy to cloud: + - AWS ECS/Fargate + - Google Cloud Run + - Azure Container Instances + - DigitalOcean App Platform + +### Option 2: Traditional VPS (Ubuntu/Debian) + +1. SSH into your server + +2. Install Node.js and nginx: +```bash +curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - +sudo apt-get install -y nodejs nginx +``` + +3. Clone repository: +```bash +git clone +cd yaltopia-ticket-admin +``` + +4. Install dependencies and build: +```bash +npm ci +npm run build:prod +``` + +5. Configure nginx: +```bash +sudo cp nginx.conf /etc/nginx/sites-available/yaltopia-admin +sudo ln -s /etc/nginx/sites-available/yaltopia-admin /etc/nginx/sites-enabled/ +sudo nginx -t +sudo systemctl reload nginx +``` + +6. Copy build files: +```bash +sudo cp -r dist/* /var/www/html/ +``` + +## Post-Deployment + +### 1. Verification +- [ ] Application loads correctly +- [ ] All routes work (test deep links) +- [ ] API calls successful +- [ ] Authentication works +- [ ] No console errors +- [ ] Performance acceptable (Lighthouse score) + +### 2. Monitoring Setup +- [ ] Error tracking active +- [ ] Analytics tracking +- [ ] Uptime monitoring configured +- [ ] Alert notifications set up + +### 3. Backup & Rollback Plan +- [ ] Previous version tagged in git +- [ ] Rollback procedure documented +- [ ] Database backup (if applicable) + +## Continuous Deployment + +### GitHub Actions (Automated) + +The `.github/workflows/ci.yml` file is configured for CI, and `.github/workflows/deploy.yml` builds production artifacts. + +For automated deployment, you can extend the workflow to: + +1. **Push Docker image to registry:** +```yaml +- name: Login to Docker Registry + uses: docker/login-action@v3 + with: + registry: ${{ secrets.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + +- name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + ${{ secrets.DOCKER_REGISTRY }}/yaltopia-admin:latest + ${{ secrets.DOCKER_REGISTRY }}/yaltopia-admin:${{ github.sha }} +``` + +2. **Deploy to your server via SSH:** +```yaml +- name: Deploy to production server + uses: appleboy/ssh-action@v1.0.0 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + script: | + cd /opt/yaltopia-admin + docker pull ${{ secrets.DOCKER_REGISTRY }}/yaltopia-admin:latest + docker-compose down + docker-compose up -d +``` + +## Troubleshooting + +### Build Fails +- Check Node.js version (18+) +- Clear node_modules and reinstall: `rm -rf node_modules package-lock.json && npm install` +- Check for TypeScript errors: `npm run type-check` + +### Blank Page After Deploy +- Check browser console for errors +- Verify API URL is correct +- Check nginx/server configuration for SPA routing +- Verify all environment variables are set + +### API Calls Failing +- Check CORS configuration on backend +- Verify API URL in environment variables +- Check network tab in browser DevTools +- Verify authentication token handling + +### Performance Issues +- Analyze bundle size: `npm run build -- --mode production` +- Check for large dependencies +- Implement code splitting +- Enable compression (gzip/brotli) +- Use CDN for static assets + +## Security Hardening + +### 1. Content Security Policy (CSP) + +Add to nginx.conf or hosting platform headers: + +``` +Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.yourdomain.com; +``` + +### 2. Additional Security Headers + +``` +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-XSS-Protection: 1; mode=block +Referrer-Policy: strict-origin-when-cross-origin +Permissions-Policy: geolocation=(), microphone=(), camera=() +``` + +### 3. Rate Limiting + +Implement on backend and consider using Cloudflare or similar CDN with DDoS protection. + +## Maintenance + +### Regular Tasks +- [ ] Update dependencies monthly: `npm update` +- [ ] Security audit: `npm audit` +- [ ] Review error logs weekly +- [ ] Monitor performance metrics +- [ ] Backup configuration and data + +### Updates +1. Test updates in development +2. Deploy to staging +3. Run full test suite +4. Deploy to production during low-traffic period +5. Monitor for issues + +## Support + +For issues or questions: +- Check logs in error tracking service +- Review browser console errors +- Check server logs +- Contact backend team for API issues diff --git a/dev-docs/DEVELOPMENT.md b/dev-docs/DEVELOPMENT.md new file mode 100644 index 0000000..967f437 --- /dev/null +++ b/dev-docs/DEVELOPMENT.md @@ -0,0 +1,294 @@ +# Development Guide + +Complete guide for developing the Yaltopia Ticket Admin application. + +## Tech Stack + +- **Frontend**: React 18 + TypeScript + Vite +- **UI**: TailwindCSS + shadcn/ui +- **State**: React Query (TanStack Query) +- **Routing**: React Router v6 +- **HTTP Client**: Axios +- **Forms**: React Hook Form + Zod +- **Charts**: Recharts +- **Notifications**: Sonner +- **Error Tracking**: Sentry +- **Testing**: Vitest + Testing Library + +## Quick Start + +```bash +# Install dependencies +npm install + +# Set up environment +cp .env.example .env +# Edit .env with your backend URL + +# Start development server +npm run dev + +# Run tests +npm run test + +# Build for production +npm run build +``` + +## Project Structure + +``` +src/ +├── components/ # Reusable UI components +├── pages/ # Page components +├── services/ # API service layer +│ ├── api/ +│ │ └── client.ts # Axios instance +│ ├── auth.service.ts +│ ├── user.service.ts +│ └── ... +├── layouts/ # Layout components +├── lib/ # Utilities +└── test/ # Test utilities +``` + +## API Architecture + +### Service Layer Pattern + +All API calls go through typed service classes: + +``` +Component → Service → API Client → Backend +``` + +### Available Services + +```typescript +import { + authService, // Authentication + userService, // User management + analyticsService, // Analytics + securityService, // Security + systemService, // System health + announcementService,// Announcements + auditService, // Audit logs + settingsService // Settings +} from '@/services' +``` + +### Usage Examples + +**Fetching Data:** +```typescript +import { useQuery } from '@tanstack/react-query' +import { userService } from '@/services' + +const { data, isLoading } = useQuery({ + queryKey: ['users'], + queryFn: () => userService.getUsers({ page: 1, limit: 20 }) +}) +``` + +**Mutations:** +```typescript +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { userService } from '@/services' + +const queryClient = useQueryClient() +const mutation = useMutation({ + mutationFn: (id: string) => userService.deleteUser(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['users'] }) + toast.success('User deleted') + } +}) +``` + +**Direct Calls:** +```typescript +import { authService } from '@/services' + +const response = await authService.login({ email, password }) +``` + +## Authentication + +### Setup + +1. Backend must return tokens on login +2. Frontend stores in httpOnly cookies (recommended) or localStorage +3. All requests automatically include auth token +4. 401 errors trigger automatic token refresh + +### Login Flow + +```typescript +// User logs in +const response = await authService.login({ email, password }) + +// Token stored automatically +// User redirected to dashboard + +// All subsequent requests include token +await userService.getUsers() // Token added automatically +``` + +### Protected Routes + +```typescript +}> + } /> + +``` + +### Logout + +```typescript +await authService.logout() // Clears tokens & cookies +navigate('/login') +``` + +## API Standards + +### Service Methods + +All service methods: +- Return typed data (no `response.data` unwrapping needed) +- Throw errors with `error.response.data.message` +- Use consistent naming (get, create, update, delete) + +### Error Handling + +```typescript +try { + await userService.deleteUser(id) + toast.success('User deleted') +} catch (error: any) { + toast.error(error.response?.data?.message || 'Operation failed') +} +``` + +### Type Safety + +```typescript +// All responses are typed +const users: PaginatedResponse = await userService.getUsers() +const stats: OverviewStats = await analyticsService.getOverview() +``` + +## Environment Variables + +```bash +# Required +VITE_BACKEND_API_URL=http://localhost:3001/api/v1 + +# Optional (Sentry) +VITE_SENTRY_DSN=your-sentry-dsn +VITE_SENTRY_ENVIRONMENT=development +``` + +## Common Tasks + +### Adding a New Service Method + +```typescript +// src/services/user.service.ts +async exportUserData(userId: string): Promise { + const response = await apiClient.get(`/admin/users/${userId}/export`, { + responseType: 'blob' + }) + return response.data +} +``` + +### Adding a New Page + +1. Create page component in `src/pages/` +2. Add route in `src/App.tsx` +3. Import required services +4. Use React Query for data fetching + +### Adding a New Component + +1. Create in `src/components/` +2. Use TypeScript for props +3. Follow existing patterns +4. Add to component exports if reusable + +## Best Practices + +### React Query + +```typescript +// Good - specific query keys +queryKey: ['users', page, limit, search] + +// Bad - too generic +queryKey: ['data'] +``` + +### Service Layer + +```typescript +// Good - use services +import { userService } from '@/services' +await userService.getUsers() + +// Bad - direct axios +import axios from 'axios' +await axios.get('/api/users') +``` + +### Error Handling + +```typescript +// Good - handle errors +try { + await userService.deleteUser(id) +} catch (error: any) { + toast.error(error.response?.data?.message) +} + +// Bad - no error handling +await userService.deleteUser(id) +``` + +### Type Safety + +```typescript +// Good - use types +const users: PaginatedResponse = await userService.getUsers() + +// Bad - any type +const users: any = await userService.getUsers() +``` + +## Troubleshooting + +### CORS Errors +- Ensure backend has CORS configured +- Check `withCredentials: true` in API client +- Verify `VITE_BACKEND_API_URL` is correct + +### 401 Errors +- Check token is being sent +- Verify backend token validation +- Check token expiration + +### Build Errors +- Run `npm run build` to check TypeScript errors +- Fix any type errors +- Ensure all imports are correct + +### Test Failures +- Run `npm run test` to see failures +- Check mock implementations +- Verify test data matches types + +## Additional Resources + +- [Testing Guide](./TESTING.md) +- [Deployment Guide](./DEPLOYMENT.md) +- [Security Guide](./SECURITY.md) +- [Troubleshooting](./TROUBLESHOOTING.md) diff --git a/dev-docs/README.md b/dev-docs/README.md new file mode 100644 index 0000000..f2b2c27 --- /dev/null +++ b/dev-docs/README.md @@ -0,0 +1,101 @@ +# Developer Documentation + +Essential documentation for the Yaltopia Ticket Admin project. + +## 📚 Documentation + +### [Development Guide](./DEVELOPMENT.md) +Complete development guide including: +- Tech stack & project structure +- Quick start & setup +- Common tasks & best practices +- Troubleshooting + +### [API & Service Layer Guide](./API_GUIDE.md) ⭐ +**Essential reading for making API calls:** +- Service layer architecture +- All available services & methods +- Common patterns & examples +- Error handling +- Best practices + +### [Testing Guide](./TESTING_GUIDE.md) +Testing setup and practices: +- Unit testing with Vitest +- Component testing +- Integration testing +- Test utilities & mocks + +### [Deployment Guide](./DEPLOYMENT.md) +Production deployment: +- Pre-deployment checklist +- Deployment options (Docker, VPS) +- Environment configuration +- CI/CD setup + +### [Security Guide](./SECURITY.md) +Security best practices: +- Authentication & authorization +- Data protection +- Security headers +- CORS configuration +- Input validation + +## 🚀 Quick Start + +```bash +# Install +npm install + +# Configure +cp .env.example .env +# Edit .env with your backend URL + +# Develop +npm run dev + +# Test +npm run test + +# Build +npm run build +``` + +## 📖 Key Concepts + +### Service Layer +All API calls go through typed service classes: +```typescript +import { userService } from '@/services' +const users = await userService.getUsers() +``` + +### React Query +Data fetching with caching: +```typescript +const { data } = useQuery({ + queryKey: ['users'], + queryFn: () => userService.getUsers() +}) +``` + +### Protected Routes +Authentication required for admin routes: +```typescript +}> + } /> + +``` + +## 🆘 Need Help? + +1. **Making API calls?** → [API & Service Layer Guide](./API_GUIDE.md) +2. **General development?** → [Development Guide](./DEVELOPMENT.md) +3. **Writing tests?** → [Testing Guide](./TESTING_GUIDE.md) +4. **Deploying?** → [Deployment Guide](./DEPLOYMENT.md) +5. **Security questions?** → [Security Guide](./SECURITY.md) + +--- + +**Last Updated:** 2024 +**Maintained By:** Development Team diff --git a/dev-docs/SECURITY.md b/dev-docs/SECURITY.md new file mode 100644 index 0000000..eec2028 --- /dev/null +++ b/dev-docs/SECURITY.md @@ -0,0 +1,339 @@ +# Security Guide + +## Current Security Implementation + +### Authentication +- JWT tokens stored in localStorage +- Automatic token attachment to API requests via Axios interceptor +- Automatic redirect to login on 401 (Unauthorized) responses +- Token removal on logout + +### API Security +- HTTPS enforcement (production requirement) +- CORS configuration on backend +- Error handling for common HTTP status codes (401, 403, 404, 500) +- Network error handling + +### Client-Side Protection +- Error boundary for graceful error handling +- Input validation on forms +- XSS protection via React's built-in escaping + +## Security Improvements for Production + +### 1. Token Storage (High Priority) + +**Current Issue:** Tokens in localStorage are vulnerable to XSS attacks. + +**Recommended Solution:** Use httpOnly cookies + +Backend changes needed: +```javascript +// Set cookie on login +res.cookie('access_token', token, { + httpOnly: true, + secure: true, // HTTPS only + sameSite: 'strict', + maxAge: 24 * 60 * 60 * 1000 // 24 hours +}); +``` + +Frontend changes: +```typescript +// Remove localStorage token handling +// Cookies are automatically sent with requests +// Update api-client.ts to remove token interceptor +``` + +### 2. Content Security Policy (CSP) + +Add to your hosting platform or nginx configuration: + +``` +Content-Security-Policy: + default-src 'self'; + script-src 'self' 'unsafe-inline' 'unsafe-eval'; + style-src 'self' 'unsafe-inline'; + img-src 'self' data: https:; + font-src 'self' data:; + connect-src 'self' https://api.yourdomain.com; + frame-ancestors 'none'; +``` + +**Note:** Adjust `unsafe-inline` and `unsafe-eval` as you refine your CSP. + +### 3. HTTPS Enforcement + +**Required for production!** + +- Obtain SSL/TLS certificate (Let's Encrypt, Cloudflare, etc.) +- Configure your hosting platform to enforce HTTPS +- Add HSTS header: + +``` +Strict-Transport-Security: max-age=31536000; includeSubDomains; preload +``` + +### 4. Input Validation & Sanitization + +Always validate and sanitize user inputs: + +```typescript +// Example: Email validation +const validateEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +}; + +// Example: Sanitize HTML (if displaying user content) +import DOMPurify from 'dompurify'; +const clean = DOMPurify.sanitize(dirty); +``` + +### 5. Rate Limiting + +Implement on backend: +- Login attempts: 5 per 15 minutes per IP +- API calls: 100 per minute per user +- Password reset: 3 per hour per email + +Consider using: +- express-rate-limit (Node.js) +- Cloudflare rate limiting +- API Gateway rate limiting (AWS, Azure, GCP) + +### 6. Dependency Security + +Regular security audits: + +```bash +# Check for vulnerabilities +npm audit + +# Fix automatically (if possible) +npm audit fix + +# Update dependencies +npm update + +# Check for outdated packages +npm outdated +``` + +Set up automated dependency updates: +- Dependabot (GitHub) +- Renovate Bot +- Snyk + +### 7. Error Handling + +**Don't expose sensitive information in errors:** + +```typescript +// Bad +catch (error) { + toast.error(error.message); // May expose stack traces +} + +// Good +catch (error) { + console.error('Error details:', error); // Log for debugging + toast.error('An error occurred. Please try again.'); // Generic message +} +``` + +### 8. Secrets Management + +**Never commit secrets to git:** + +- Use environment variables +- Use secret management services (AWS Secrets Manager, Azure Key Vault, etc.) +- Add `.env*` to `.gitignore` ✓ +- Use different secrets for dev/staging/production + +### 9. API Key Protection + +If using third-party APIs: + +```typescript +// Bad - API key in client code +const API_KEY = 'sk_live_123456789'; + +// Good - Proxy through your backend +const response = await fetch('/api/proxy/third-party-service', { + method: 'POST', + body: JSON.stringify(data) +}); +``` + +### 10. Session Management + +Implement proper session handling: + +- Session timeout after inactivity (15-30 minutes) +- Logout on browser close (optional) +- Single session per user (optional) +- Session invalidation on password change + +```typescript +// Example: Auto-logout on inactivity +let inactivityTimer: NodeJS.Timeout; + +const resetInactivityTimer = () => { + clearTimeout(inactivityTimer); + inactivityTimer = setTimeout(() => { + // Logout user + localStorage.removeItem('access_token'); + window.location.href = '/login'; + }, 30 * 60 * 1000); // 30 minutes +}; + +// Reset timer on user activity +document.addEventListener('mousemove', resetInactivityTimer); +document.addEventListener('keypress', resetInactivityTimer); +``` + +## Security Headers Checklist + +Configure these headers on your server/hosting platform: + +- [x] `X-Frame-Options: SAMEORIGIN` ✓ +- [x] `X-Content-Type-Options: nosniff` ✓ +- [x] `X-XSS-Protection: 1; mode=block` ✓ +- [x] `Referrer-Policy: strict-origin-when-cross-origin` ✓ +- [ ] `Strict-Transport-Security: max-age=31536000; includeSubDomains` +- [ ] `Content-Security-Policy: ...` +- [ ] `Permissions-Policy: geolocation=(), microphone=(), camera=()` + +## Monitoring & Incident Response + +### 1. Error Tracking + +Implement error tracking service: + +```typescript +// Example: Sentry integration +import * as Sentry from "@sentry/react"; + +Sentry.init({ + dsn: import.meta.env.VITE_SENTRY_DSN, + environment: import.meta.env.VITE_ENV, + tracesSampleRate: 1.0, +}); +``` + +### 2. Security Monitoring + +Monitor for: +- Failed login attempts +- Unusual API usage patterns +- Error rate spikes +- Slow response times +- Unauthorized access attempts + +### 3. Incident Response Plan + +1. **Detection:** Monitor logs and alerts +2. **Assessment:** Determine severity and impact +3. **Containment:** Isolate affected systems +4. **Eradication:** Remove threat +5. **Recovery:** Restore normal operations +6. **Lessons Learned:** Document and improve + +## Compliance Considerations + +### GDPR (if applicable) +- User data encryption +- Right to be forgotten +- Data export functionality +- Privacy policy +- Cookie consent + +### HIPAA (if handling health data) +- Additional encryption requirements +- Audit logging +- Access controls +- Business Associate Agreements + +### PCI DSS (if handling payments) +- Never store credit card data in frontend +- Use payment gateway (Stripe, PayPal, etc.) +- Secure transmission (HTTPS) + +## Security Testing + +### Manual Testing +- [ ] Test authentication flows +- [ ] Test authorization (role-based access) +- [ ] Test input validation +- [ ] Test error handling +- [ ] Test session management + +### Automated Testing +- [ ] OWASP ZAP scan +- [ ] npm audit +- [ ] Lighthouse security audit +- [ ] SSL Labs test (https://www.ssllabs.com/ssltest/) + +### Penetration Testing +Consider hiring security professionals for: +- Vulnerability assessment +- Penetration testing +- Security code review + +## Security Checklist for Production + +- [ ] HTTPS enabled with valid certificate +- [ ] All security headers configured +- [ ] CSP implemented +- [ ] Tokens stored securely (httpOnly cookies) +- [ ] Input validation on all forms +- [ ] Rate limiting configured +- [ ] Error tracking service active +- [ ] Regular security audits scheduled +- [ ] Dependency updates automated +- [ ] Secrets properly managed +- [ ] Backup and recovery plan in place +- [ ] Incident response plan documented +- [ ] Team trained on security best practices + +## Resources + +- [OWASP Top 10](https://owasp.org/www-project-top-ten/) +- [MDN Web Security](https://developer.mozilla.org/en-US/docs/Web/Security) +- [React Security Best Practices](https://react.dev/learn/security) +- [Vite Security](https://vitejs.dev/guide/security.html) + +## Reporting Security Issues + +If you discover a security vulnerability: + +1. **Do not** open a public issue +2. Email security@yourdomain.com +3. Include detailed description and steps to reproduce +4. Allow reasonable time for fix before disclosure + +## Regular Security Tasks + +### Daily +- Monitor error logs +- Check failed login attempts + +### Weekly +- Review security alerts +- Check for dependency updates + +### Monthly +- Run security audit: `npm audit` +- Update dependencies +- Review access logs + +### Quarterly +- Security training for team +- Review and update security policies +- Penetration testing (if budget allows) + +### Annually +- Comprehensive security audit +- Update incident response plan +- Review compliance requirements diff --git a/dev-docs/TESTING_GUIDE.md b/dev-docs/TESTING_GUIDE.md new file mode 100644 index 0000000..d815ea2 --- /dev/null +++ b/dev-docs/TESTING_GUIDE.md @@ -0,0 +1,118 @@ +# Testing Guide + +## Overview +This project uses **Vitest** and **React Testing Library** for testing. + +## Running Tests + +```bash +# Run tests in watch mode +npm run test + +# Run tests once +npm run test:run + +# Run tests with UI +npm run test:ui + +# Run tests with coverage +npm run test:coverage +``` + +## Test Structure + +``` +src/ +├── components/ +│ ├── __tests__/ +│ │ └── ProtectedRoute.test.tsx +│ └── ProtectedRoute.tsx +├── lib/ +│ ├── __tests__/ +│ │ └── utils.test.ts +│ └── utils.ts +├── pages/ +│ └── login/ +│ ├── __tests__/ +│ │ └── index.test.tsx +│ └── index.tsx +└── test/ + ├── setup.ts # Test setup + └── test-utils.tsx # Custom render with providers +``` + +## Writing Tests + +### Component Tests + +```typescript +import { describe, it, expect } from 'vitest' +import { render, screen } from '@/test/test-utils' +import MyComponent from '../MyComponent' + +describe('MyComponent', () => { + it('should render correctly', () => { + render() + expect(screen.getByText('Hello')).toBeInTheDocument() + }) +}) +``` + +### Testing with User Interactions + +```typescript +import userEvent from '@testing-library/user-event' + +it('should handle click', async () => { + const user = userEvent.setup() + render() + + await user.click(screen.getByRole('button')) + expect(screen.getByText('Clicked')).toBeInTheDocument() +}) +``` + +### Testing API Calls + +```typescript +import { vi } from 'vitest' +import { adminApiHelpers } from '@/lib/api-client' + +vi.mock('@/lib/api-client', () => ({ + adminApiHelpers: { + getUsers: vi.fn(), + }, +})) + +it('should fetch users', async () => { + const mockGetUsers = vi.mocked(adminApiHelpers.getUsers) + mockGetUsers.mockResolvedValue({ data: [] }) + + // Test component that calls getUsers +}) +``` + +## Coverage Goals + +- **Statements**: 80%+ +- **Branches**: 75%+ +- **Functions**: 80%+ +- **Lines**: 80%+ + +## Best Practices + +1. **Test behavior, not implementation** +2. **Use semantic queries** (getByRole, getByLabelText) +3. **Avoid testing implementation details** +4. **Mock external dependencies** +5. **Keep tests simple and focused** +6. **Use descriptive test names** + +## CI Integration + +Tests run automatically on: +- Every push to main/develop +- Every pull request +- Before deployment + +See `.github/workflows/ci.yml` for details. diff --git a/eslint.config.js b/eslint.config.js index 5e6b472..4d7c104 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,21 +3,34 @@ import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' -export default defineConfig([ - globalIgnores(['dist']), +export default tseslint.config( + { ignores: ['dist'] }, { + extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + '@typescript-eslint/no-explicit-any': 'warn', // Change from error to warning + }, }, -]) + { + files: ['src/test/**/*.{ts,tsx}'], + rules: { + 'react-refresh/only-export-components': 'off', + '@typescript-eslint/no-explicit-any': 'off', // Allow any in test files + }, + }, +) diff --git a/index.html b/index.html index c1b6187..9fe4b63 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + @@ -10,7 +10,7 @@ href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" /> - yaltopia-ticket-admin + Yaltopia Ticket Admin
diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..0403ba0 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,34 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # SPA routing - serve index.html for all routes + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Disable access to hidden files + location ~ /\. { + deny all; + } +} diff --git a/package-lock.json b/package-lock.json index 6f552ce..c633af8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "yaltopia-ticket-admin", - "version": "0.0.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "yaltopia-ticket-admin", - "version": "0.0.0", + "version": "1.0.0", "dependencies": { "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-dialog": "^1.1.15", @@ -19,8 +19,9 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toast": "^1.2.15", + "@sentry/react": "^10.39.0", "@tanstack/react-query": "^5.90.12", - "axios": "^1.13.2", + "axios": "^1.13.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -34,23 +35,44 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", + "@vitest/coverage-v8": "^4.0.18", + "@vitest/ui": "^4.0.18", "autoprefixer": "^10.4.23", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "jsdom": "^28.1.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.17", "tailwindcss-animate": "^1.0.7", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "^7.2.4" + "vite": "^7.2.4", + "vitest": "^4.0.18" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -64,14 +86,72 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "node_modules/@asamuzakjp/css-color": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -80,9 +160,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "license": "MIT", "engines": { @@ -90,22 +170,22 @@ } }, "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==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", "peer": true, "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", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -122,14 +202,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", + "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@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" @@ -139,13 +219,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -166,29 +246,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -198,9 +278,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { @@ -238,27 +318,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -299,34 +379,44 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -334,9 +424,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { @@ -347,6 +437,161 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.28.tgz", + "integrity": "sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -790,9 +1035,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "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": { @@ -873,20 +1118,20 @@ } }, "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==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", + "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "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", + "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" }, "engines": { @@ -910,9 +1155,9 @@ } }, "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==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", "dev": true, "license": "MIT", "engines": { @@ -946,32 +1191,50 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz", + "integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.3", + "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", + "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.4" + "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", @@ -1124,6 +1387,13 @@ "node": ">= 8" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -2860,9 +3130,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz", - "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", + "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" ], @@ -2874,9 +3144,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz", - "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", + "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" ], @@ -2888,9 +3158,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz", - "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", + "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" ], @@ -2902,9 +3172,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz", - "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", + "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" ], @@ -2916,9 +3186,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz", - "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", + "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" ], @@ -2930,9 +3200,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz", - "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", + "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" ], @@ -2944,9 +3214,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz", - "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", + "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" ], @@ -2958,9 +3228,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz", - "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", + "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" ], @@ -2972,9 +3242,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz", - "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", + "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" ], @@ -2986,9 +3256,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz", - "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==", + "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" ], @@ -3000,9 +3270,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz", - "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", + "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", + "optional": true, + "os": [ + "linux" + ] + }, + "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" ], @@ -3014,9 +3298,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz", - "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", + "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", + "optional": true, + "os": [ + "linux" + ] + }, + "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" ], @@ -3028,9 +3326,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz", - "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", + "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" ], @@ -3042,9 +3340,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz", - "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", + "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" ], @@ -3056,9 +3354,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz", - "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", + "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" ], @@ -3070,9 +3368,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz", - "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", + "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" ], @@ -3084,9 +3382,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz", - "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", + "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" ], @@ -3097,10 +3395,24 @@ "linux" ] }, + "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", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz", - "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", + "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" ], @@ -3112,9 +3424,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz", - "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", + "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" ], @@ -3126,9 +3438,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz", - "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", + "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" ], @@ -3140,9 +3452,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz", - "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==", + "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" ], @@ -3154,9 +3466,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz", - "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==", + "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" ], @@ -3167,6 +3479,97 @@ "win32" ] }, + "node_modules/@sentry-internal/browser-utils": { + "version": "10.39.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.39.0.tgz", + "integrity": "sha512-W6WODonMGiI13Az5P7jd/m2lj/JpIyuVKg7wE4X+YdlMehLspAv6I7gRE4OBSumS14ZjdaYDpD/lwtnBwKAzcA==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.39.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "10.39.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.39.0.tgz", + "integrity": "sha512-cRXmmDeOr5FzVsBNRLU4WDEuC3fhuD0XV362EWl4DI3XBGao8ukaueKcLIKic5WZx6uXimjWw/UJmDLgxeCqkg==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.39.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "10.39.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.39.0.tgz", + "integrity": "sha512-obZoYOrUfxIYBHkmtPpItRdE38VuzF1VIxSgZ8Mbtq/9UvCWh+eOaVWU2stN/cVu1KYuYX0nQwBvdN28L6y/JA==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.39.0", + "@sentry/core": "10.39.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "10.39.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.39.0.tgz", + "integrity": "sha512-TTiX0XWCcqTqFGJjEZYObk93j/sJmXcqPzcu0cN2mIkKnnaHDY3w74SHZCshKqIr0AOQdt1HDNa36s3TCdt0Jw==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "10.39.0", + "@sentry/core": "10.39.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/browser": { + "version": "10.39.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.39.0.tgz", + "integrity": "sha512-I50W/1PDJWyqgNrGufGhBYCmmO3Bb159nx2Ut2bKoVveTfgH/hLEtDyW0kHo8Fu454mW+ukyXfU4L4s+kB9aaw==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.39.0", + "@sentry-internal/feedback": "10.39.0", + "@sentry-internal/replay": "10.39.0", + "@sentry-internal/replay-canvas": "10.39.0", + "@sentry/core": "10.39.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/core": { + "version": "10.39.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.39.0.tgz", + "integrity": "sha512-xCLip2mBwCdRrvXHtVEULX0NffUTYZZBhEUGht0WFL+GNdNQ7gmBOGOczhZlrf2hgFFtDO0fs1xiP9bqq5orEQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/react": { + "version": "10.39.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.39.0.tgz", + "integrity": "sha512-qxReWHFhDcXNGEyAlYzhR7+K70es+vXaSknTZui1q7TfQwCT1rZlLKn/K8GDpNsb35RC5QhiIphU6pKbyYgZqw==", + "license": "MIT", + "dependencies": { + "@sentry/browser": "10.39.0", + "@sentry/core": "10.39.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -3180,9 +3583,9 @@ "license": "MIT" }, "node_modules/@tanstack/query-core": { - "version": "5.90.12", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.12.tgz", - "integrity": "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==", + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", "license": "MIT", "funding": { "type": "github", @@ -3190,12 +3593,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.90.12", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.12.tgz", - "integrity": "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==", + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.20.tgz", + "integrity": "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.90.12" + "@tanstack/query-core": "5.90.20" }, "funding": { "type": "github", @@ -3205,6 +3608,104 @@ "react": "^18 || ^19" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3250,6 +3751,17 @@ "@babel/types": "^7.28.2" } }, + "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": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -3293,9 +3805,9 @@ } }, "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -3313,6 +3825,13 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "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, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3328,9 +3847,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz", - "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", + "version": "24.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", "dev": true, "license": "MIT", "peer": true, @@ -3339,9 +3858,9 @@ } }, "node_modules/@types/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", - "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "version": "19.2.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", + "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", "devOptional": true, "license": "MIT", "peer": true, @@ -3367,20 +3886,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz", - "integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/type-utils": "8.50.0", - "@typescript-eslint/utils": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3390,8 +3909,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.50.0", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -3406,18 +3925,18 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz", - "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3427,20 +3946,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz", - "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.50.0", - "@typescript-eslint/types": "^8.50.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3454,14 +3973,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz", - "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3472,9 +3991,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz", - "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", "dev": true, "license": "MIT", "engines": { @@ -3489,17 +4008,17 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz", - "integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0", - "@typescript-eslint/utils": "8.50.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3509,14 +4028,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", - "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, "license": "MIT", "engines": { @@ -3528,21 +4047,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz", - "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.50.0", - "@typescript-eslint/tsconfig-utils": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", - "debug": "^4.3.4", - "minimatch": "^9.0.4", - "semver": "^7.6.0", + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3555,36 +4074,49 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -3595,16 +4127,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz", - "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3614,19 +4146,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz", - "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3636,6 +4168,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitejs/plugin-react": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", @@ -3657,6 +4202,170 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.18", + "ast-v8-to-istanbul": "^0.3.10", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "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", + "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/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "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 + }, + "vite": { + "optional": true + } + } + }, + "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": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.18.tgz", + "integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.18" + } + }, + "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", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -3681,10 +4390,20 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "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": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -3698,6 +4417,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "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", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -3735,19 +4465,6 @@ "node": ">= 8" } }, - "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", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -3774,6 +4491,45 @@ "node": ">=10" } }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "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", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", + "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3781,9 +4537,9 @@ "license": "MIT" }, "node_modules/autoprefixer": { - "version": "10.4.23", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", - "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", "dev": true, "funding": [ { @@ -3802,7 +4558,7 @@ "license": "MIT", "dependencies": { "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", + "caniuse-lite": "^1.0.30001766", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -3818,13 +4574,13 @@ } }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -3836,15 +4592,25 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.9", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.9.tgz", - "integrity": "sha512-V8fbOCSeOFvlDj7LLChUcqbZrdKD9RU/VR260piF1790vT0mfLSwGc/Qzxv3IqiTukOpNtItePa0HBpMAj7MDg==", + "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==", "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3951,9 +4717,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001760", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", - "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "version": "1.0.30001767", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", + "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", "dev": true, "funding": [ { @@ -3971,6 +4737,16 @@ ], "license": "CC-BY-4.0" }, + "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": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4131,6 +4907,27 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4144,6 +4941,32 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.1.0.tgz", + "integrity": "sha512-Ml4fP2UT2K3CUBQnVlbdV/8aFDdlY69E+YnwJM+3VUWl08S3J8c8aRuJqCkD9Py8DHZ7zNNvsfKl8psocHZEFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.0", + "@csstools/css-syntax-patches-for-csstree": "^1.0.28", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -4182,9 +5005,9 @@ } }, "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", "license": "ISC", "engines": { "node": ">=12" @@ -4272,6 +5095,20 @@ "node": ">=12" } }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/date-fns": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", @@ -4300,6 +5137,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", @@ -4322,6 +5166,16 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -4342,6 +5196,14 @@ "dev": true, "license": "MIT" }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4357,12 +5219,25 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "version": "1.5.283", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz", + "integrity": "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==", "dev": true, "license": "ISC" }, + "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": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -4381,6 +5256,13 @@ "node": ">= 0.4" } }, + "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": "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", @@ -4409,9 +5291,9 @@ } }, "node_modules/es-toolkit": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.43.0.tgz", - "integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", "license": "MIT", "workspaces": [ "docs", @@ -4484,9 +5366,9 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", "peer": true, @@ -4497,7 +5379,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", + "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -4623,9 +5505,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "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": "BSD-3-Clause", "dependencies": { @@ -4658,6 +5540,16 @@ "node": ">=4.0" } }, + "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": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -4669,11 +5561,21 @@ } }, "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==", "license": "MIT" }, + "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": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4726,32 +5628,21 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } + "license": "MIT" }, "node_modules/file-entry-cache": { "version": "8.0.0", @@ -5051,6 +5942,54 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.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==", + "dev": true, + "license": "MIT" + }, + "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": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "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": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5098,6 +6037,16 @@ "node": ">=0.8.19" } }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -5169,6 +6118,13 @@ "node": ">=0.12.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5176,6 +6132,45 @@ "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-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-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/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -5206,6 +6201,47 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", + "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.8.1", + "@bramus/specificity": "^2.4.2", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^6.0.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "undici": "^7.21.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -5339,6 +6375,68 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "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.5" + } + }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5348,6 +6446,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5372,19 +6477,6 @@ "node": ">=8.6" } }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -5406,10 +6498,20 @@ "node": ">= 0.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "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": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", "dev": true, "license": "ISC", "dependencies": { @@ -5419,6 +6521,16 @@ "node": "*" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5501,6 +6613,17 @@ "node": ">= 6" } }, + "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/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5564,6 +6687,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5591,6 +6727,13 @@ "dev": true, "license": "MIT" }, + "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": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5599,14 +6742,14 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", "peer": true, "engines": { - "node": ">=12" + "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -5707,9 +6850,9 @@ } }, "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", "dev": true, "funding": [ { @@ -5723,21 +6866,28 @@ ], "license": "MIT", "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" + "lilconfig": "^3.1.1" }, "engines": { - "node": ">= 14" + "node": ">= 18" }, "peerDependencies": { + "jiti": ">=1.21.0", "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { + "jiti": { + "optional": true + }, "postcss": { "optional": true }, - "ts-node": { + "tsx": { + "optional": true + }, + "yaml": { "optional": true } } @@ -5799,6 +6949,44 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": 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", @@ -5860,9 +7048,9 @@ } }, "node_modules/react-is": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", - "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", "license": "MIT", "peer": true }, @@ -5948,9 +7136,9 @@ } }, "node_modules/react-router": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.11.0.tgz", - "integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", + "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -5970,12 +7158,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.11.0.tgz", - "integrity": "sha512-e49Ir/kMGRzFOOrYQBdoitq3ULigw4lKbAyKusnvtDu2t4dBX4AGYPrzNvorXmVuOyeakai6FUPW5MmibvVG8g==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz", + "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", "license": "MIT", "dependencies": { - "react-router": "7.11.0" + "react-router": "7.13.0" }, "engines": { "node": ">=20.0.0" @@ -6030,23 +7218,10 @@ "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/recharts": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.6.0.tgz", - "integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz", + "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==", "license": "MIT", "workspaces": [ "www" @@ -6073,6 +7248,20 @@ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", @@ -6089,6 +7278,16 @@ "redux": "^5.0.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/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", @@ -6138,9 +7337,9 @@ } }, "node_modules/rollup": { - "version": "4.53.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz", - "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==", + "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": { @@ -6154,28 +7353,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.5", - "@rollup/rollup-android-arm64": "4.53.5", - "@rollup/rollup-darwin-arm64": "4.53.5", - "@rollup/rollup-darwin-x64": "4.53.5", - "@rollup/rollup-freebsd-arm64": "4.53.5", - "@rollup/rollup-freebsd-x64": "4.53.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", - "@rollup/rollup-linux-arm-musleabihf": "4.53.5", - "@rollup/rollup-linux-arm64-gnu": "4.53.5", - "@rollup/rollup-linux-arm64-musl": "4.53.5", - "@rollup/rollup-linux-loong64-gnu": "4.53.5", - "@rollup/rollup-linux-ppc64-gnu": "4.53.5", - "@rollup/rollup-linux-riscv64-gnu": "4.53.5", - "@rollup/rollup-linux-riscv64-musl": "4.53.5", - "@rollup/rollup-linux-s390x-gnu": "4.53.5", - "@rollup/rollup-linux-x64-gnu": "4.53.5", - "@rollup/rollup-linux-x64-musl": "4.53.5", - "@rollup/rollup-openharmony-arm64": "4.53.5", - "@rollup/rollup-win32-arm64-msvc": "4.53.5", - "@rollup/rollup-win32-ia32-msvc": "4.53.5", - "@rollup/rollup-win32-x64-gnu": "4.53.5", - "@rollup/rollup-win32-x64-msvc": "4.53.5", + "@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" } }, @@ -6203,6 +7405,19 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -6248,6 +7463,28 @@ "node": ">=8" } }, + "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/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -6268,6 +7505,33 @@ "node": ">=0.10.0" } }, + "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": "MIT" + }, + "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/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6330,6 +7594,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", @@ -6341,9 +7612,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "dev": true, "license": "MIT", "peer": true, @@ -6356,7 +7627,7 @@ "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.21.6", + "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", @@ -6365,7 +7636,7 @@ "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", @@ -6418,6 +7689,23 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "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" + }, + "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", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -6435,6 +7723,67 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/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", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", + "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.23" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", + "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6448,10 +7797,46 @@ "node": ">=8.0" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "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", "engines": { @@ -6503,16 +7888,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.0.tgz", - "integrity": "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", + "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.50.0", - "@typescript-eslint/parser": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0", - "@typescript-eslint/utils": "8.50.0" + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6522,10 +7907,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -6656,9 +8051,9 @@ } }, "node_modules/vite": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", - "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "peer": true, @@ -6731,6 +8126,176 @@ } } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/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": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "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/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6747,6 +8312,23 @@ "node": ">= 8" } }, + "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": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -6757,6 +8339,23 @@ "node": ">=0.10.0" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -6764,22 +8363,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -6794,9 +8377,9 @@ } }, "node_modules/zod": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", - "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", "peer": true, diff --git a/package.json b/package.json index 8a02d2d..bd50229 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,20 @@ { "name": "yaltopia-ticket-admin", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", + "build:prod": "tsc -b && vite build --mode production", "lint": "eslint .", - "preview": "vite preview" + "lint:fix": "eslint . --fix", + "preview": "vite preview", + "type-check": "tsc -b --noEmit", + "test": "vitest", + "test:ui": "vitest --ui", + "test:run": "vitest run", + "test:coverage": "vitest run --coverage" }, "dependencies": { "@radix-ui/react-avatar": "^1.1.11", @@ -21,8 +28,9 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toast": "^1.2.15", + "@sentry/react": "^10.39.0", "@tanstack/react-query": "^5.90.12", - "axios": "^1.13.2", + "axios": "^1.13.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -36,20 +44,27 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", + "@vitest/coverage-v8": "^4.0.18", + "@vitest/ui": "^4.0.18", "autoprefixer": "^10.4.23", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "jsdom": "^28.1.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.17", "tailwindcss-animate": "^1.0.7", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "^7.2.4" + "vite": "^7.2.4", + "vitest": "^4.0.18" } } diff --git a/public/admin-icon.svg b/public/admin-icon.svg new file mode 100644 index 0000000..ee6fe69 --- /dev/null +++ b/public/admin-icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 8fa5041..7bd9aeb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,13 +1,12 @@ import { Navigate, Route, Routes } from "react-router-dom" import { AppShell } from "@/layouts/app-shell" +import { ProtectedRoute } from "@/components/ProtectedRoute" +import LoginPage from "@/pages/login" import DashboardPage from "@/pages/admin/dashboard" import UsersPage from "@/pages/admin/users" import UserDetailsPage from "@/pages/admin/users/[id]" import UserActivityPage from "@/pages/admin/users/[id]/activity" -import LogsPage from "@/pages/admin/logs" -import ErrorLogsPage from "@/pages/admin/logs/errors" -import AccessLogsPage from "@/pages/admin/logs/access" -import LogDetailsPage from "@/pages/admin/logs/[id]" +import ActivityLogPage from "@/pages/activity-log" import SettingsPage from "@/pages/admin/settings" import MaintenancePage from "@/pages/admin/maintenance" import AnnouncementsPage from "@/pages/admin/announcements" @@ -25,20 +24,28 @@ import AnalyticsRevenuePage from "@/pages/admin/analytics/revenue" import AnalyticsStoragePage from "@/pages/admin/analytics/storage" import AnalyticsApiPage from "@/pages/admin/analytics/api" import HealthPage from "@/pages/admin/health" +import NotificationsPage from "@/pages/notifications" function App() { return ( - }> + } /> + + + + } + > } /> } /> } /> } /> } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> } /> } /> } /> @@ -56,6 +63,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..ea071d8 --- /dev/null +++ b/src/components/ErrorBoundary.tsx @@ -0,0 +1,87 @@ +import { Component } from 'react'; +import type { ErrorInfo, ReactNode } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { AlertCircle } from 'lucide-react'; +import { Sentry } from '@/lib/sentry'; + +interface Props { + children: ReactNode; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +export class ErrorBoundary extends Component { + public state: State = { + hasError: false, + error: null, + }; + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Uncaught error:', error, errorInfo); + + // Log to Sentry + Sentry.captureException(error, { + contexts: { + react: { + componentStack: errorInfo.componentStack, + }, + }, + }); + } + + private handleReset = () => { + this.setState({ hasError: false, error: null }); + window.location.href = '/'; + }; + + public render() { + if (this.state.hasError) { + return ( +
+ + + + + Something went wrong + + + +

+ An unexpected error occurred. The error has been logged and we'll look into it. Please try refreshing the page or contact support if the problem persists. +

+ {import.meta.env.DEV && this.state.error && ( +
+

+ {this.state.error.message} +

+
+ )} +
+ + +
+
+
+
+ ); + } + + return this.props.children; + } +} diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..6aa2d85 --- /dev/null +++ b/src/components/ProtectedRoute.tsx @@ -0,0 +1,17 @@ +import { Navigate, useLocation } from "react-router-dom" + +interface ProtectedRouteProps { + children: React.ReactNode +} + +export function ProtectedRoute({ children }: ProtectedRouteProps) { + const location = useLocation() + const token = localStorage.getItem('access_token') + + if (!token) { + // Redirect to login page with return URL + return + } + + return <>{children} +} diff --git a/src/components/__tests__/ErrorBoundary.test.tsx b/src/components/__tests__/ErrorBoundary.test.tsx new file mode 100644 index 0000000..242e615 --- /dev/null +++ b/src/components/__tests__/ErrorBoundary.test.tsx @@ -0,0 +1,92 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen } from '@/test/test-utils' +import { ErrorBoundary } from '../ErrorBoundary' +import { Sentry } from '@/lib/sentry' + +// Mock Sentry +vi.mock('@/lib/sentry', () => ({ + Sentry: { + captureException: vi.fn(), + }, +})) + +// Component that throws an error +const ThrowError = ({ shouldThrow }: { shouldThrow: boolean }) => { + if (shouldThrow) { + throw new Error('Test error') + } + return
No error
+} + +describe('ErrorBoundary', () => { + beforeEach(() => { + vi.clearAllMocks() + // Suppress console.error for cleaner test output + vi.spyOn(console, 'error').mockImplementation(() => {}) + }) + + it('should render children when there is no error', () => { + render( + +
Test Content
+
+ ) + + expect(screen.getByText('Test Content')).toBeInTheDocument() + }) + + it('should render error UI when an error is thrown', () => { + render( + + + + ) + + expect(screen.getByText('Something went wrong')).toBeInTheDocument() + expect(screen.getByText(/An unexpected error occurred/i)).toBeInTheDocument() + expect(screen.getByRole('button', { name: /Go to Home/i })).toBeInTheDocument() + expect(screen.getByRole('button', { name: /Refresh Page/i })).toBeInTheDocument() + }) + + it('should log error to Sentry', () => { + render( + + + + ) + + expect(Sentry.captureException).toHaveBeenCalled() + }) + + it('should show error message in development mode', () => { + const originalEnv = import.meta.env.DEV + import.meta.env.DEV = true + + render( + + + + ) + + expect(screen.getByText('Test error')).toBeInTheDocument() + + import.meta.env.DEV = originalEnv + }) + + it('should handle refresh button click', () => { + render( + + + + ) + + const refreshButton = screen.getByRole('button', { name: /Refresh Page/i }) + + // Verify button exists and is clickable + expect(refreshButton).toBeInTheDocument() + expect(refreshButton).toBeEnabled() + + // Note: We can't easily test window.location.reload() in jsdom + // The button's onClick handler calls window.location.reload() which is tested manually + }) +}) diff --git a/src/components/__tests__/ProtectedRoute.test.tsx b/src/components/__tests__/ProtectedRoute.test.tsx new file mode 100644 index 0000000..585b9ad --- /dev/null +++ b/src/components/__tests__/ProtectedRoute.test.tsx @@ -0,0 +1,36 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { render, screen } from '@/test/test-utils' +import { ProtectedRoute } from '../ProtectedRoute' + +describe('ProtectedRoute', () => { + beforeEach(() => { + // Clear localStorage before each test + localStorage.clear() + vi.clearAllMocks() + }) + + it('should redirect to login when no token exists', () => { + render( + +
Protected Content
+
+ ) + + // Should not render protected content + expect(screen.queryByText('Protected Content')).not.toBeInTheDocument() + }) + + it('should render children when token exists', () => { + // Set token in localStorage + localStorage.setItem('access_token', 'fake-token') + + render( + +
Protected Content
+
+ ) + + // Should render protected content + expect(screen.getByText('Protected Content')).toBeInTheDocument() + }) +}) diff --git a/src/components/ui/badge-variants.ts b/src/components/ui/badge-variants.ts new file mode 100644 index 0000000..1bd9724 --- /dev/null +++ b/src/components/ui/badge-variants.ts @@ -0,0 +1,21 @@ +import { cva } from "class-variance-authority" + +export const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index e87d62b..0abefc2 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -1,27 +1,8 @@ import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import { type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" - -const badgeVariants = cva( - "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) +import { badgeVariants } from "./badge-variants" export interface BadgeProps extends React.HTMLAttributes, @@ -33,4 +14,4 @@ function Badge({ className, variant, ...props }: BadgeProps) { ) } -export { Badge, badgeVariants } +export { Badge } diff --git a/src/components/ui/button-variants.ts b/src/components/ui/button-variants.ts new file mode 100644 index 0000000..7f96851 --- /dev/null +++ b/src/components/ui/button-variants.ts @@ -0,0 +1,31 @@ +import { cva } from "class-variance-authority" + +export const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 65d4fcd..25fa2bd 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,38 +1,9 @@ import * as React from "react" import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import { type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" - -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", - outline: - "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", - icon: "h-9 w-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) +import { buttonVariants } from "./button-variants" export interface ButtonProps extends React.ButtonHTMLAttributes, @@ -54,4 +25,4 @@ const Button = React.forwardRef( ) Button.displayName = "Button" -export { Button, buttonVariants } +export { Button } diff --git a/src/layouts/app-shell.tsx b/src/layouts/app-shell.tsx index a51461e..e264b7f 100644 --- a/src/layouts/app-shell.tsx +++ b/src/layouts/app-shell.tsx @@ -1,4 +1,5 @@ -import { Outlet, Link, useLocation } from "react-router-dom" +import { Outlet, Link, useLocation, useNavigate } from "react-router-dom" +import { useState } from "react" import { LayoutDashboard, Users, @@ -11,16 +12,30 @@ import { Activity, Heart, Search, - Mail, Bell, LogOut, } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Avatar, AvatarFallback } from "@/components/ui/avatar" -import { Separator } from "@/components/ui/separator" -import { Card, CardContent } from "@/components/ui/card" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" import { cn } from "@/lib/utils" +import { authService } from "@/services" +import { toast } from "sonner" + +interface User { + email: string + firstName?: string + lastName?: string + role: string +} const adminNavigationItems = [ { icon: LayoutDashboard, label: "Dashboard", path: "/admin/dashboard" }, @@ -37,6 +52,22 @@ const adminNavigationItems = [ export function AppShell() { const location = useLocation() + const navigate = useNavigate() + const [searchQuery, setSearchQuery] = useState("") + + // Initialize user from localStorage + const [user] = useState(() => { + const userStr = localStorage.getItem('user') + if (userStr) { + try { + return JSON.parse(userStr) + } catch (error) { + console.error('Failed to parse user data:', error) + return null + } + } + return null + }) const isActive = (path: string) => { return location.pathname.startsWith(path) @@ -50,6 +81,49 @@ export function AppShell() { return item?.label || "Admin Panel" } + const handleLogout = async () => { + await authService.logout() + navigate('/login', { replace: true }) + } + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault() + if (searchQuery.trim()) { + const currentPath = location.pathname + navigate(`${currentPath}?search=${encodeURIComponent(searchQuery)}`) + toast.success(`Searching for: ${searchQuery}`) + } + } + + const handleSearchChange = (e: React.ChangeEvent) => { + setSearchQuery(e.target.value) + } + + const handleNotificationClick = () => { + navigate('/notifications') + } + + const handleProfileClick = () => { + navigate('/admin/settings') + } + + const getUserInitials = () => { + if (user?.firstName && user?.lastName) { + return `${user.firstName[0]}${user.lastName[0]}`.toUpperCase() + } + if (user?.email) { + return user.email.substring(0, 2).toUpperCase() + } + return 'AD' + } + + const getUserDisplayName = () => { + if (user?.firstName && user?.lastName) { + return `${user.firstName} ${user.lastName}` + } + return user?.email || 'Admin User' + } + return (
{/* Sidebar */} @@ -88,21 +162,18 @@ export function AppShell() {
- AD + {getUserInitials()}
-

Admin User

-

admin@example.com

+

{getUserDisplayName()}

+

{user?.email || 'admin@example.com'}

- - - AD - + + + + + + +
+

{getUserDisplayName()}

+

{user?.email}

+
+
+ + + + Profile Settings + + navigate('/notifications')}> + + Notifications + + + + + Logout + +
+
diff --git a/src/lib/__tests__/utils.test.ts b/src/lib/__tests__/utils.test.ts new file mode 100644 index 0000000..dbc545b --- /dev/null +++ b/src/lib/__tests__/utils.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest' +import { cn } from '../utils' + +describe('cn utility', () => { + it('should merge class names', () => { + const result = cn('class1', 'class2') + expect(result).toBe('class1 class2') + }) + + it('should handle conditional classes', () => { + const isConditional = true + const isHidden = false + const result = cn('base', isConditional && 'conditional', isHidden && 'hidden') + expect(result).toBe('base conditional') + }) + + it('should merge Tailwind classes correctly', () => { + const result = cn('px-2 py-1', 'px-4') + expect(result).toBe('py-1 px-4') + }) + + it('should handle arrays of classes', () => { + const result = cn(['class1', 'class2'], 'class3') + expect(result).toBe('class1 class2 class3') + }) + + it('should handle objects with boolean values', () => { + const result = cn({ + 'class1': true, + 'class2': false, + 'class3': true, + }) + expect(result).toBe('class1 class3') + }) + + it('should handle undefined and null values', () => { + const result = cn('class1', undefined, null, 'class2') + expect(result).toBe('class1 class2') + }) + + it('should handle empty input', () => { + const result = cn() + expect(result).toBe('') + }) + + it('should merge conflicting Tailwind classes', () => { + const result = cn('text-red-500', 'text-blue-500') + expect(result).toBe('text-blue-500') + }) +}) diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts deleted file mode 100644 index 25571fc..0000000 --- a/src/lib/api-client.ts +++ /dev/null @@ -1,277 +0,0 @@ -import axios, { type AxiosInstance, type AxiosError } from 'axios'; - -const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1'; - -// Create axios instance -const adminApi: AxiosInstance = axios.create({ - baseURL: API_BASE_URL, - headers: { - 'Content-Type': 'application/json', - }, -}); - -// Add token interceptor -adminApi.interceptors.request.use( - (config) => { - const token = localStorage.getItem('access_token'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; - }, - (error) => { - return Promise.reject(error); - } -); - -// Add response interceptor for error handling -adminApi.interceptors.response.use( - (response) => response, - (error: AxiosError) => { - if (error.response?.status === 401) { - // Redirect to login - localStorage.removeItem('access_token'); - window.location.href = '/login'; - } else if (error.response?.status === 403) { - // Show access denied - const message = error.response?.data?.message || 'You do not have permission to access this resource'; - if (typeof window !== 'undefined') { - import('sonner').then(({ toast }) => { - toast.error(message); - }); - } - } else if (error.response?.status === 404) { - const message = error.response?.data?.message || 'Resource not found'; - if (typeof window !== 'undefined') { - import('sonner').then(({ toast }) => { - toast.error(message); - }); - } - } else if (error.response?.status === 500) { - const message = error.response?.data?.message || 'Server error occurred. Please try again later.'; - if (typeof window !== 'undefined') { - import('sonner').then(({ toast }) => { - toast.error(message); - }); - } - } else if (!error.response) { - // Network error - if (typeof window !== 'undefined') { - import('sonner').then(({ toast }) => { - toast.error('Network error. Please check your connection.'); - }); - } - } - return Promise.reject(error); - } -); - -// API helper functions -export const adminApiHelpers = { - // Users - getUsers: (params?: { - page?: number; - limit?: number; - role?: string; - isActive?: boolean; - search?: string; - }) => adminApi.get('/admin/users', { params }), - - getUser: (id: string) => adminApi.get(`/admin/users/${id}`), - - getUserActivity: (id: string, days: number = 30) => - adminApi.get(`/admin/users/${id}/activity`, { params: { days } }), - - updateUser: (id: string, data: { - role?: string; - isActive?: boolean; - firstName?: string; - lastName?: string; - }) => adminApi.put(`/admin/users/${id}`, data), - - deleteUser: (id: string, hard: boolean = false) => - adminApi.delete(`/admin/users/${id}?hard=${hard}`), - - resetPassword: (id: string) => - adminApi.post(`/admin/users/${id}/reset-password`), - - exportUsers: (format: string = 'csv') => - adminApi.post('/admin/users/export', null, { params: { format } }), - - importUsers: (file: File) => { - const formData = new FormData(); - formData.append('file', file); - return adminApi.post('/admin/users/import', formData, { - headers: { 'Content-Type': 'multipart/form-data' }, - }); - }, - - // Logs - getLogs: (params?: { - page?: number; - limit?: number; - level?: string; - type?: string; - userId?: string; - startDate?: string; - endDate?: string; - search?: string; - minDuration?: number; - }) => adminApi.get('/admin/logs', { params }), - - getErrorLogs: (params?: { - page?: number; - limit?: number; - userId?: string; - startDate?: string; - endDate?: string; - }) => adminApi.get('/admin/logs/errors', { params }), - - getAccessLogs: (params?: { - page?: number; - limit?: number; - userId?: string; - startDate?: string; - endDate?: string; - }) => adminApi.get('/admin/logs/access', { params }), - - getLogById: (id: string) => adminApi.get(`/admin/logs/${id}`), - - getLogStats: (startDate?: string, endDate?: string) => - adminApi.get('/admin/logs/stats/summary', { params: { startDate, endDate } }), - - exportLogs: (params: { - format?: string; - level?: string; - startDate?: string; - endDate?: string; - }) => adminApi.post('/admin/logs/export', null, { params }), - - cleanupLogs: (days: number = 30) => - adminApi.post('/admin/logs/cleanup', null, { params: { days } }), - - // Analytics - getOverview: () => adminApi.get('/admin/analytics/overview'), - - getUserGrowth: (days: number = 30) => - adminApi.get('/admin/analytics/users/growth', { params: { days } }), - - getRevenue: (period: string = '30days') => - adminApi.get('/admin/analytics/revenue', { params: { period } }), - - getStorageAnalytics: () => adminApi.get('/admin/analytics/storage'), - - getApiUsage: (days: number = 7) => - adminApi.get('/admin/analytics/api-usage', { params: { days } }), - - getErrorRate: (days: number = 7) => - adminApi.get('/admin/analytics/error-rate', { params: { days } }), - - // System - getHealth: () => adminApi.get('/admin/system/health'), - - getSystemInfo: () => adminApi.get('/admin/system/info'), - - getSettings: (category?: string) => - adminApi.get('/admin/system/settings', { params: { category } }), - - getSetting: (key: string) => adminApi.get(`/admin/system/settings/${key}`), - - createSetting: (data: { - key: string; - value: string; - category: string; - description?: string; - isPublic?: boolean; - }) => adminApi.post('/admin/system/settings', data), - - updateSetting: (key: string, data: { - value: string; - description?: string; - isPublic?: boolean; - }) => adminApi.put(`/admin/system/settings/${key}`, data), - - deleteSetting: (key: string) => adminApi.delete(`/admin/system/settings/${key}`), - - // Maintenance - getMaintenanceStatus: () => adminApi.get('/admin/maintenance'), - - enableMaintenance: (message?: string) => - adminApi.post('/admin/maintenance/enable', { message }), - - disableMaintenance: () => adminApi.post('/admin/maintenance/disable'), - - // Announcements - getAnnouncements: (activeOnly: boolean = false) => - adminApi.get('/admin/announcements', { params: { activeOnly } }), - - createAnnouncement: (data: { - title: string; - message: string; - type?: string; - priority?: number; - targetAudience?: string; - startsAt?: string; - endsAt?: string; - }) => adminApi.post('/admin/announcements', data), - - updateAnnouncement: (id: string, data: { - title?: string; - message?: string; - type?: string; - priority?: number; - targetAudience?: string; - startsAt?: string; - endsAt?: string; - }) => adminApi.put(`/admin/announcements/${id}`, data), - - toggleAnnouncement: (id: string) => - adminApi.patch(`/admin/announcements/${id}/toggle`), - - deleteAnnouncement: (id: string) => - adminApi.delete(`/admin/announcements/${id}`), - - // Audit - getAuditLogs: (params?: { - page?: number; - limit?: number; - userId?: string; - action?: string; - resourceType?: string; - resourceId?: string; - startDate?: string; - endDate?: string; - }) => adminApi.get('/admin/audit/logs', { params }), - - getUserAuditActivity: (userId: string, days: number = 30) => - adminApi.get(`/admin/audit/users/${userId}`, { params: { days } }), - - getResourceHistory: (type: string, id: string) => - adminApi.get(`/admin/audit/resource/${type}/${id}`), - - getAuditStats: (startDate?: string, endDate?: string) => - adminApi.get('/admin/audit/stats', { params: { startDate, endDate } }), - - // Security - getFailedLogins: (params?: { - page?: number; - limit?: number; - email?: string; - ipAddress?: string; - }) => adminApi.get('/admin/security/failed-logins', { params }), - - getSuspiciousActivity: () => adminApi.get('/admin/security/suspicious-activity'), - - getAllApiKeys: () => adminApi.get('/admin/security/api-keys'), - - revokeApiKey: (id: string) => - adminApi.patch(`/admin/security/api-keys/${id}/revoke`), - - getRateLimitViolations: (days: number = 7) => - adminApi.get('/admin/security/rate-limits', { params: { days } }), - - getActiveSessions: () => adminApi.get('/admin/security/sessions'), -}; - -export default adminApi; - diff --git a/src/lib/error-tracker.ts b/src/lib/error-tracker.ts new file mode 100644 index 0000000..91167d9 --- /dev/null +++ b/src/lib/error-tracker.ts @@ -0,0 +1,192 @@ +import { Sentry } from './sentry' +import apiClient from '@/services/api/client' + +interface ErrorLog { + message: string + stack?: string + url: string + userAgent: string + timestamp: string + userId?: string + extra?: Record +} + +class ErrorTracker { + private queue: ErrorLog[] = [] + private isProcessing = false + private maxQueueSize = 50 + + /** + * Track an error with fallback to backend if Sentry fails + */ + async trackError( + error: Error, + context?: { + tags?: Record + extra?: Record + userId?: string + } + ) { + const errorLog: ErrorLog = { + message: error.message, + stack: error.stack, + url: window.location.href, + userAgent: navigator.userAgent, + timestamp: new Date().toISOString(), + userId: context?.userId, + extra: context?.extra, + } + + // Try Sentry first + try { + Sentry.captureException(error, { + tags: context?.tags, + extra: context?.extra, + }) + } catch (sentryError) { + console.warn('Sentry failed, using fallback:', sentryError) + // If Sentry fails, queue for backend logging + this.queueError(errorLog) + } + + // Always log to backend as backup + this.queueError(errorLog) + } + + /** + * Track a message with fallback + */ + async trackMessage( + message: string, + level: 'info' | 'warning' | 'error' = 'info', + extra?: Record + ) { + try { + Sentry.captureMessage(message, level) + } catch (sentryError) { + console.warn('Sentry failed, using fallback:', sentryError) + this.queueError({ + message, + url: window.location.href, + userAgent: navigator.userAgent, + timestamp: new Date().toISOString(), + extra: { level, ...extra }, + }) + } + } + + /** + * Queue error for backend logging + */ + private queueError(errorLog: ErrorLog) { + this.queue.push(errorLog) + + // Prevent queue from growing too large + if (this.queue.length > this.maxQueueSize) { + this.queue.shift() + } + + // Process queue + this.processQueue() + } + + /** + * Send queued errors to backend + */ + private async processQueue() { + if (this.isProcessing || this.queue.length === 0) { + return + } + + this.isProcessing = true + + while (this.queue.length > 0) { + const errorLog = this.queue[0] + + try { + // Send to your backend error logging endpoint + await apiClient.post('/errors/log', errorLog) + this.queue.shift() // Remove from queue on success + } catch (error) { + console.error('Failed to log error to backend:', error) + // Keep in queue and try again later + break + } + } + + this.isProcessing = false + } + + /** + * Set user context for tracking + */ + setUser(user: { id: string; email?: string; name?: string }) { + try { + Sentry.setUser({ + id: user.id, + email: user.email, + username: user.name, + }) + } catch (error) { + console.warn('Failed to set Sentry user:', error) + } + } + + /** + * Clear user context (on logout) + */ + clearUser() { + try { + Sentry.setUser(null) + } catch (error) { + console.warn('Failed to clear Sentry user:', error) + } + } + + /** + * Add breadcrumb for debugging + */ + addBreadcrumb( + category: string, + message: string, + level: 'info' | 'warning' | 'error' = 'info' + ) { + try { + Sentry.addBreadcrumb({ + category, + message, + level, + }) + } catch (error) { + console.warn('Failed to add Sentry breadcrumb:', error) + } + } +} + +// Export singleton instance +export const errorTracker = new ErrorTracker() + +// Global error handler +window.addEventListener('error', (event) => { + errorTracker.trackError(new Error(event.message), { + extra: { + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + }, + }) +}) + +// Unhandled promise rejection handler +window.addEventListener('unhandledrejection', (event) => { + errorTracker.trackError( + event.reason instanceof Error + ? event.reason + : new Error(String(event.reason)), + { + extra: { + type: 'unhandledRejection', + }, + } + ) +}) diff --git a/src/lib/sentry.ts b/src/lib/sentry.ts new file mode 100644 index 0000000..ae1da4d --- /dev/null +++ b/src/lib/sentry.ts @@ -0,0 +1,46 @@ +import * as Sentry from '@sentry/react' + +export const initSentry = () => { + const dsn = import.meta.env.VITE_SENTRY_DSN + const environment = import.meta.env.VITE_ENV || 'development' + + // Only initialize Sentry if DSN is provided and not in development + if (dsn && environment !== 'development') { + Sentry.init({ + dsn, + environment, + integrations: [ + Sentry.browserTracingIntegration(), + Sentry.replayIntegration({ + maskAllText: true, + blockAllMedia: true, + }), + ], + // Performance Monitoring + tracesSampleRate: environment === 'production' ? 0.1 : 1.0, + // Session Replay + replaysSessionSampleRate: 0.1, + replaysOnErrorSampleRate: 1.0, + // Error filtering + beforeSend(event, hint) { + // Filter out errors from browser extensions + if (event.exception) { + const error = hint.originalException + if (error && typeof error === 'object' && 'message' in error) { + const message = String(error.message) + if ( + message.includes('chrome-extension://') || + message.includes('moz-extension://') + ) { + return null + } + } + } + return event + }, + }) + } +} + +// Export Sentry for manual error logging +export { Sentry } diff --git a/src/main.tsx b/src/main.tsx index 450da0d..f31ddc9 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,14 +6,22 @@ import { BrowserRouter } from "react-router-dom" import { QueryClientProvider } from "@tanstack/react-query" import { queryClient } from "@/app/query-client" import { Toaster } from "@/components/ui/toast" +import { ErrorBoundary } from "@/components/ErrorBoundary" +import { initSentry } from "@/lib/sentry" +import "@/lib/error-tracker" // Initialize global error handlers + +// Initialize Sentry +initSentry() createRoot(document.getElementById('root')!).render( - - - - - - + + + + + + + + , ) diff --git a/src/pages/activity-log/index.tsx b/src/pages/activity-log/index.tsx index 981b464..9c83c47 100644 --- a/src/pages/activity-log/index.tsx +++ b/src/pages/activity-log/index.tsx @@ -1,3 +1,5 @@ +import { useState } from "react" +import { useQuery } from "@tanstack/react-query" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -10,14 +12,60 @@ import { TableHeader, TableRow, } from "@/components/ui/table" -import { Search, Download, Eye, MoreVertical } from "lucide-react" +import { Search, Download, Eye, ChevronLeft, ChevronRight } from "lucide-react" +import { auditService, type AuditLog } from "@/services" +import { format } from "date-fns" export default function ActivityLogPage() { + const [page, setPage] = useState(1) + const [limit] = useState(20) + const [search, setSearch] = useState("") + const [actionFilter, setActionFilter] = useState("") + const [resourceTypeFilter, setResourceTypeFilter] = useState("") + + const { data: auditData, isLoading } = useQuery({ + queryKey: ['activity-log', page, limit, search, actionFilter, resourceTypeFilter], + queryFn: async () => { + const params: Record = { page, limit } + if (search) params.search = search + if (actionFilter) params.action = actionFilter + if (resourceTypeFilter) params.resourceType = resourceTypeFilter + return await auditService.getAuditLogs(params) + }, + }) + + const handleExport = async () => { + try { + const blob = await auditService.exportAuditLogs({ format: 'csv' }) + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `activity-log-${format(new Date(), 'yyyy-MM-dd')}.csv` + document.body.appendChild(a) + a.click() + window.URL.revokeObjectURL(url) + document.body.removeChild(a) + } catch (error) { + console.error('Export failed:', error) + } + } + + const getActionBadgeColor = (action: string) => { + const colors: Record = { + Create: "bg-blue-500", + Update: "bg-green-500", + Delete: "bg-red-500", + Login: "bg-purple-500", + Logout: "bg-gray-500", + } + return colors[action] || "bg-gray-500" + } + return (

Activity Log

- @@ -33,109 +81,113 @@ export default function ActivityLogPage() { setSearch(e.target.value)} />
- setActionFilter(e.target.value)} + > + + + + + + - setResourceTypeFilter(e.target.value)} + > + + + + + -
- - - - Log ID - User - Action - Entity - Description - IP Address - Timestamp - Action - - - - - LOG001 - john.smith@example.com - - Create - - Client - Created new client record - 192.168.1.1 - 2024-01-15 10:30:45 - -
-
+ + + Log ID + User + Action + Resource + Resource ID + IP Address + Timestamp + Actions + + + + {auditData?.data?.map((log: AuditLog) => ( + + {log.id} + {log.userId || 'N/A'} + + + {log.action} + + + {log.resourceType} + {log.resourceId} + {log.ipAddress || 'N/A'} + + {format(new Date(log.timestamp), 'MMM dd, yyyy HH:mm:ss')} + + + + + + ))} + +
+ {auditData?.data?.length === 0 && ( +
+ No activity logs found +
+ )} + {auditData && auditData.totalPages > 1 && ( +
+
+ Page {auditData.page} of {auditData.totalPages} ({auditData.total} total) +
+
+ -
- - - - LOG002 - jane.doe@example.com - - Update - - Subscription - Updated subscription status - 192.168.1.2 - 2024-01-15 09:15:22 - -
- - -
-
-
- - LOG003 - admin@example.com - - Login - - System - User logged in successfully - 192.168.1.3 - 2024-01-15 08:00:00 - -
- - -
-
-
- - +
+ )} + + )}
diff --git a/src/pages/admin/analytics/api.tsx b/src/pages/admin/analytics/api.tsx index 6aa9d6f..f5d4304 100644 --- a/src/pages/admin/analytics/api.tsx +++ b/src/pages/admin/analytics/api.tsx @@ -8,23 +8,18 @@ import { TableHeader, TableRow, } from "@/components/ui/table" -import { adminApiHelpers } from "@/lib/api-client" +import { analyticsService } from "@/services" +import type { ApiUsageData } from "@/types/analytics.types" export default function AnalyticsApiPage() { const { data: apiUsage, isLoading } = useQuery({ queryKey: ['admin', 'analytics', 'api-usage'], - queryFn: async () => { - const response = await adminApiHelpers.getApiUsage(7) - return response.data - }, + queryFn: () => analyticsService.getApiUsage(7), }) const { data: errorRate, isLoading: errorRateLoading } = useQuery({ queryKey: ['admin', 'analytics', 'error-rate'], - queryFn: async () => { - const response = await adminApiHelpers.getErrorRate(7) - return response.data - }, + queryFn: () => analyticsService.getErrorRate(7), }) return ( @@ -90,11 +85,11 @@ export default function AnalyticsApiPage() { - {apiUsage?.map((endpoint: any, index: number) => ( + {apiUsage?.map((endpoint: ApiUsageData, index: number) => ( - {endpoint.endpoint} - {endpoint.calls} - {endpoint.avgDuration?.toFixed(2) || 'N/A'} + {endpoint.date} + {endpoint.requests} + {endpoint.avgResponseTime?.toFixed(2) || 'N/A'} ))} diff --git a/src/pages/admin/analytics/index.tsx b/src/pages/admin/analytics/index.tsx index 8451611..2e706cc 100644 --- a/src/pages/admin/analytics/index.tsx +++ b/src/pages/admin/analytics/index.tsx @@ -1,5 +1,4 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Button } from "@/components/ui/button" import { BarChart3, Users, DollarSign, HardDrive, Activity } from "lucide-react" import { useNavigate } from "react-router-dom" diff --git a/src/pages/admin/analytics/revenue.tsx b/src/pages/admin/analytics/revenue.tsx index 2fa11b1..44c1794 100644 --- a/src/pages/admin/analytics/revenue.tsx +++ b/src/pages/admin/analytics/revenue.tsx @@ -1,15 +1,12 @@ import { useQuery } from "@tanstack/react-query" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts" -import { adminApiHelpers } from "@/lib/api-client" +import { analyticsService } from "@/services" export default function AnalyticsRevenuePage() { const { data: revenue, isLoading } = useQuery({ queryKey: ['admin', 'analytics', 'revenue'], - queryFn: async () => { - const response = await adminApiHelpers.getRevenue('90days') - return response.data - }, + queryFn: () => analyticsService.getRevenue('90days'), }) return ( diff --git a/src/pages/admin/analytics/storage.tsx b/src/pages/admin/analytics/storage.tsx index 67a73d9..56e3c52 100644 --- a/src/pages/admin/analytics/storage.tsx +++ b/src/pages/admin/analytics/storage.tsx @@ -1,17 +1,20 @@ import { useQuery } from "@tanstack/react-query" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from "recharts" -import { adminApiHelpers } from "@/lib/api-client" +import { analyticsService } from "@/services" +import type { StorageByUser, StorageAnalytics } from "@/types/analytics.types" const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8'] +interface ChartDataItem { + name: string + value: number +} + export default function AnalyticsStoragePage() { - const { data: storage, isLoading } = useQuery({ + const { data: storage, isLoading } = useQuery({ queryKey: ['admin', 'analytics', 'storage'], - queryFn: async () => { - const response = await adminApiHelpers.getStorageAnalytics() - return response.data - }, + queryFn: () => analyticsService.getStorageAnalytics(), }) const formatBytes = (bytes: number) => { @@ -22,7 +25,7 @@ export default function AnalyticsStoragePage() { return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i] } - const chartData = storage?.byCategory?.map((cat: any) => ({ + const chartData: ChartDataItem[] = storage?.byCategory?.map((cat) => ({ name: cat.category, value: cat.size, })) || [] @@ -71,12 +74,12 @@ export default function AnalyticsStoragePage() { cx="50%" cy="50%" labelLine={false} - label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`} + label={({ name, percent }) => `${name} ${((percent ?? 0) * 100).toFixed(0)}%`} outerRadius={80} fill="#8884d8" dataKey="value" > - {chartData.map((entry: any, index: number) => ( + {chartData.map((_entry: ChartDataItem, index: number) => ( ))} @@ -100,13 +103,13 @@ export default function AnalyticsStoragePage() {
- {storage.topUsers.map((user: any, index: number) => ( + {storage.topUsers.map((user: StorageByUser, index: number) => (
-

{user.user}

-

{user.files} files

+

{user.userName || user.email}

+

{user.documentCount} files

-

{formatBytes(user.size)}

+

{formatBytes(user.storageUsed)}

))}
diff --git a/src/pages/admin/analytics/users.tsx b/src/pages/admin/analytics/users.tsx index 9d4377a..b4e16d6 100644 --- a/src/pages/admin/analytics/users.tsx +++ b/src/pages/admin/analytics/users.tsx @@ -1,15 +1,12 @@ import { useQuery } from "@tanstack/react-query" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts" -import { adminApiHelpers } from "@/lib/api-client" +import { analyticsService } from "@/services" export default function AnalyticsUsersPage() { const { data: userGrowth, isLoading } = useQuery({ queryKey: ['admin', 'analytics', 'users', 'growth'], - queryFn: async () => { - const response = await adminApiHelpers.getUserGrowth(90) - return response.data - }, + queryFn: () => analyticsService.getUserGrowth(90), }) return ( diff --git a/src/pages/admin/announcements/index.tsx b/src/pages/admin/announcements/index.tsx index 8eea000..714772d 100644 --- a/src/pages/admin/announcements/index.tsx +++ b/src/pages/admin/announcements/index.tsx @@ -19,41 +19,127 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Megaphone, Plus, Edit, Trash2 } from "lucide-react" -import { adminApiHelpers } from "@/lib/api-client" +import { Plus, Edit, Trash2 } from "lucide-react" +import { announcementService, type Announcement, type CreateAnnouncementData } from "@/services" import { toast } from "sonner" import { format } from "date-fns" +import type { ApiError } from "@/types/error.types" export default function AnnouncementsPage() { const queryClient = useQueryClient() - const [createDialogOpen, setCreateDialogOpen] = useState(false) const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) - const [selectedAnnouncement, setSelectedAnnouncement] = useState(null) + const [formDialogOpen, setFormDialogOpen] = useState(false) + const [selectedAnnouncement, setSelectedAnnouncement] = useState(null) + const [formData, setFormData] = useState({ + title: '', + message: '', + type: 'info' as 'info' | 'warning' | 'success' | 'error', + priority: 0, + targetAudience: 'all', + startsAt: '', + endsAt: '', + }) const { data: announcements, isLoading } = useQuery({ queryKey: ['admin', 'announcements'], - queryFn: async () => { - const response = await adminApiHelpers.getAnnouncements(false) - return response.data + queryFn: () => announcementService.getAnnouncements(false), + }) + + const createMutation = useMutation({ + mutationFn: (data: CreateAnnouncementData) => announcementService.createAnnouncement(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin', 'announcements'] }) + toast.success("Announcement created successfully") + setFormDialogOpen(false) + resetForm() + }, + onError: (error) => { + const apiError = error as ApiError + toast.error(apiError.response?.data?.message || "Failed to create announcement") + }, + }) + + const updateMutation = useMutation({ + mutationFn: ({ id, data }: { id: string; data: CreateAnnouncementData }) => + announcementService.updateAnnouncement(id, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['admin', 'announcements'] }) + toast.success("Announcement updated successfully") + setFormDialogOpen(false) + resetForm() + }, + onError: (error) => { + const apiError = error as ApiError + toast.error(apiError.response?.data?.message || "Failed to update announcement") }, }) const deleteMutation = useMutation({ - mutationFn: async (id: string) => { - await adminApiHelpers.deleteAnnouncement(id) - }, + mutationFn: (id: string) => announcementService.deleteAnnouncement(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin', 'announcements'] }) toast.success("Announcement deleted successfully") setDeleteDialogOpen(false) }, - onError: (error: any) => { - toast.error(error.response?.data?.message || "Failed to delete announcement") + onError: (error) => { + const apiError = error as ApiError + toast.error(apiError.response?.data?.message || "Failed to delete announcement") }, }) + const resetForm = () => { + setFormData({ + title: '', + message: '', + type: 'info', + priority: 0, + targetAudience: 'all', + startsAt: '', + endsAt: '', + }) + setSelectedAnnouncement(null) + } + + const handleOpenCreateDialog = () => { + resetForm() + setFormDialogOpen(true) + } + + const handleOpenEditDialog = (announcement: Announcement) => { + setSelectedAnnouncement(announcement) + setFormData({ + title: announcement.title || '', + message: announcement.message || '', + type: announcement.type || 'info', + priority: announcement.priority || 0, + targetAudience: announcement.targetAudience || 'all', + startsAt: announcement.startsAt ? announcement.startsAt.split('T')[0] : '', + endsAt: announcement.endsAt ? announcement.endsAt.split('T')[0] : '', + }) + setFormDialogOpen(true) + } + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + + if (!formData.title || !formData.message) { + toast.error("Title and message are required") + return + } + + const submitData = { + ...formData, + startsAt: formData.startsAt || undefined, + endsAt: formData.endsAt || undefined, + } + + if (selectedAnnouncement) { + updateMutation.mutate({ id: selectedAnnouncement.id, data: submitData }) + } else { + createMutation.mutate(submitData) + } + } + const handleDelete = () => { if (selectedAnnouncement) { deleteMutation.mutate(selectedAnnouncement.id) @@ -64,7 +150,7 @@ export default function AnnouncementsPage() {

Announcements

- @@ -92,7 +178,7 @@ export default function AnnouncementsPage() { - {announcements?.map((announcement: any) => ( + {announcements?.map((announcement: Announcement) => ( {announcement.title} @@ -112,7 +198,11 @@ export default function AnnouncementsPage() {
-