commit ba324b370ee67aa0a78dd652780d42e004da3923 Author: Paul Bailey Date: Sun Nov 16 16:00:51 2025 +0000 Initial Commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1395f9c --- /dev/null +++ b/.env.example @@ -0,0 +1,54 @@ +# Database Configuration +DATABASE_URL=mysql://beach_litter_user:BeachClean2024!@192.168.4.20:3306/beach_litter_mapping +DB_HOST=192.168.4.20 +DB_PORT=3306 +DB_NAME=beach_litter_mapping +DB_USER=beach_litter_user +DB_PASSWORD=BeachClean2024! + +# Clerk Authentication (Frontend) +VITE_CLERK_PUBLISHABLE_KEY=YOUR_PUBLISHABLE_KEY + +# Clerk Authentication (Backend) +CLERK_SECRET_KEY=YOUR_SECRET_KEY +CLERK_WEBHOOK_SECRET=YOUR_WEBHOOK_SECRET + +# Server Configuration +PORT=3000 +NODE_ENV=development + +# File Upload +MAX_FILE_SIZE=5242880 +UPLOAD_DIR=./uploads + +# Cloud Storage (AWS S3 or Cloudinary) +CLOUD_STORAGE_PROVIDER=s3 +AWS_ACCESS_KEY_ID=your-aws-access-key +AWS_SECRET_ACCESS_KEY=your-aws-secret-key +AWS_REGION=eu-west-1 +AWS_S3_BUCKET=beach-litter-photos + +# Or for Cloudinary +# CLOUDINARY_CLOUD_NAME=your-cloud-name +# CLOUDINARY_API_KEY=your-api-key +# CLOUDINARY_API_SECRET=your-api-secret + +# Redis Cache +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= + +# External APIs +OPENWEATHER_API_KEY=your-openweather-api-key +WORLD_TIDES_API_KEY=your-world-tides-api-key + +# Email Service (SendGrid) +SENDGRID_API_KEY=your-sendgrid-api-key +FROM_EMAIL=noreply@beachlitter.ie + +# CORS +CORS_ORIGIN=http://localhost:5173 + +# Rate Limiting +RATE_LIMIT_WINDOW_MS=900000 +RATE_LIMIT_MAX_REQUESTS=100 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.kiro/specs/beach-litter-mapping/design.md b/.kiro/specs/beach-litter-mapping/design.md new file mode 100644 index 0000000..28b68b5 --- /dev/null +++ b/.kiro/specs/beach-litter-mapping/design.md @@ -0,0 +1,809 @@ +# Beach Litter Mapping Platform - Design Document + +## Overview + +The Beach Litter Mapping Platform is a full-stack web application that enables crowdsourced environmental monitoring of Irish coastal areas. The system combines modern web technologies with geospatial data processing to create an interactive platform for reporting, visualizing, and addressing beach litter and environmental concerns. + +### Technology Stack + +**Frontend:** +- React 18+ with TypeScript for type-safe component development +- Vite for build tooling +- Clerk React SDK (@clerk/clerk-react) for authentication +- Leaflet.js for interactive mapping (free, open-source alternative to Google Maps) +- OpenStreetMap tiles as the base map layer (free) +- Leaflet.heat plugin for heat map visualization +- Leaflet.markercluster for marker clustering +- React Query for server state management +- Tailwind CSS for responsive styling + +**Backend:** +- Node.js with Express.js for REST API +- MySQL 8.0+ for database with spatial data support +- Prisma ORM for database access and migrations (or Sequelize as alternative) +- Clerk for authentication and user management +- Multer for file upload handling +- exifr library for EXIF metadata extraction +- Node-cron for scheduled tasks (hotspot calculations, notifications) + +**Infrastructure:** +- Cloud storage (AWS S3 or Cloudinary) for photo storage +- Redis for caching external API responses +- Email service (SendGrid or similar) for notifications + +**External APIs:** +- OpenWeatherMap API (free tier) for weather data +- WorldTides API or NOAA for tide information +- Irish EPA Open Data Portal for water quality +- OpenStreetMap Nominatim for reverse geocoding + +## Architecture + +### System Architecture Diagram + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Client Layer │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Map View │ │ Report Form │ │ Analytics │ │ +│ │ (Leaflet) │ │ │ │ Dashboard │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Event Mgmt │ │ User Profile │ │ Notifications│ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + │ HTTPS/REST API + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ API Gateway Layer │ +│ (Express.js Router) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Auth │ │ Rate Limiting│ │ Validation │ │ +│ │ Middleware │ │ │ │ │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Service Layer │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Report │ │ Event │ │ Analytics │ │ +│ │ Service │ │ Service │ │ Service │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ User │ │ Notification │ │ External API │ │ +│ │ Service │ │ Service │ │ Service │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Data Layer │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ PostgreSQL │ │ Redis Cache │ │ Cloud Storage│ │ +│ │ + PostGIS │ │ │ │ (Photos) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ External Services │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Weather API │ │ Tide API │ │ EPA Data │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Design Patterns + +1. **Repository Pattern**: Abstracts data access logic from business logic +2. **Service Layer Pattern**: Encapsulates business logic separate from API routes +3. **Dependency Injection**: Services receive dependencies through constructors +4. **Factory Pattern**: For creating complex objects like notifications +5. **Observer Pattern**: For event-driven notifications + +## Components and Interfaces + +### Frontend Components + +#### 1. Map Interface Component + + +**Purpose**: Interactive map displaying litter reports and environmental data + +**Key Features**: +- Leaflet map with OpenStreetMap tiles +- Custom marker icons for different litter types +- Marker clustering for dense areas +- Heat map overlay toggle +- Filter controls (date, type, quantity) +- Click handlers for marker details +- Geolocation button for user positioning + +**Props Interface**: +```typescript +interface MapInterfaceProps { + reports: LitterReport[]; + events: CleanupEvent[]; + filters: FilterState; + onMarkerClick: (reportId: string) => void; + onMapClick: (lat: number, lng: number) => void; + showHeatmap: boolean; + showClusters: boolean; +} +``` + +#### 2. Report Form Component + +**Purpose**: Multi-step form for submitting litter reports + +**Key Features**: +- Photo upload with drag-and-drop +- EXIF metadata extraction on upload +- Location selection (auto from EXIF, device GPS, or map picker) +- Litter categorization dropdown +- Quantity estimation slider +- Optional description textarea +- Form validation +- Progress indicator + +**Props Interface**: +```typescript +interface ReportFormProps { + onSubmit: (report: CreateReportDTO) => Promise; + initialLocation?: Coordinates; + onCancel: () => void; +} +``` + +#### 3. Analytics Dashboard Component + +**Purpose**: Data visualization and statistics display + +**Key Features**: +- Chart.js or Recharts for data visualization +- Time series charts for report trends +- Pie charts for litter type distribution +- Summary statistics cards +- Date range selector +- Export to CSV button +- Comparison metrics (vs previous period) + +#### 4. Event Management Component + +**Purpose**: Create and manage cleanup events + +**Key Features**: +- Event creation form +- Calendar date/time picker +- Location selector on map +- Participant list display +- Registration button +- Event completion form +- Photo gallery of cleanup results + +#### 5. User Profile Component + +**Purpose**: Display user contributions and impact + +**Key Features**: +- User statistics (reports submitted, events attended) +- Impact score calculation and display +- Personal map of contributions +- Achievement badges +- Activity timeline + +### Backend API Endpoints + +#### Authentication Endpoints + +Authentication is handled by Clerk. The backend uses Clerk's middleware to verify session tokens. + +``` +POST /api/webhooks/clerk # Clerk webhook for user sync +GET /api/auth/session # Verify current session (protected) +``` + +#### Report Endpoints + +``` +GET /api/reports # List reports with filters +GET /api/reports/:id # Get single report +POST /api/reports # Create new report +PATCH /api/reports/:id # Update report +DELETE /api/reports/:id # Delete report +POST /api/reports/:id/verify # Verify report +POST /api/reports/:id/dispute # Dispute report +``` + +#### Event Endpoints + +``` +GET /api/events # List events +GET /api/events/:id # Get single event +POST /api/events # Create event +PATCH /api/events/:id # Update event +DELETE /api/events/:id # Delete event +POST /api/events/:id/register # Register for event +POST /api/events/:id/complete # Mark event complete +``` + +#### Analytics Endpoints + +``` +GET /api/analytics/summary # Overall statistics +GET /api/analytics/trends # Time series data +GET /api/analytics/hotspots # Litter hotspot data +GET /api/analytics/export # Export data as CSV +``` + +#### User Endpoints + +``` +GET /api/users/:id # Get user profile +PATCH /api/users/:id # Update profile +GET /api/users/:id/reports # User's reports +GET /api/users/:id/events # User's events +PATCH /api/users/:id/preferences # Update notification preferences +``` + +#### External Data Endpoints + +``` +GET /api/external/weather/:lat/:lng # Weather data +GET /api/external/tides/:lat/:lng # Tide information +GET /api/external/water-quality/:lat/:lng # EPA water quality +``` + +## Data Models + +### Database Configuration + +**Connection Details:** +- Database: MySQL 8.0+ +- Host: 192.168.4.20 +- Port: 3306 +- Database Name: beach_litter_mapping +- User: beach_litter_user +- Connection String: `mysql://beach_litter_user:BeachClean2024!@192.168.4.20:3306/beach_litter_mapping` + +**Environment Variables:** +```env +DATABASE_URL=mysql://beach_litter_user:BeachClean2024!@192.168.4.20:3306/beach_litter_mapping +DB_HOST=192.168.4.20 +DB_PORT=3306 +DB_NAME=beach_litter_mapping +DB_USER=beach_litter_user +DB_PASSWORD=BeachClean2024! +``` + +### Authentication Configuration (Clerk) + +**Frontend Setup:** +1. Install Clerk React SDK: `npm install @clerk/clerk-react@latest` +2. Set `VITE_CLERK_PUBLISHABLE_KEY` in `.env.local` +3. Wrap app in `` in `main.tsx` +4. Use Clerk components: ``, ``, ``, ``, `` + +**Backend Setup:** +1. Install Clerk SDK: `npm install @clerk/clerk-sdk-node` +2. Set `CLERK_SECRET_KEY` for API authentication +3. Set `CLERK_WEBHOOK_SECRET` for webhook verification +4. Use Clerk middleware to protect routes +5. Set up webhook endpoint to sync user data + +**Clerk Environment Variables:** +```env +# Frontend (.env.local) +VITE_CLERK_PUBLISHABLE_KEY=pk_test_... + +# Backend (.env) +CLERK_SECRET_KEY=sk_test_... +CLERK_WEBHOOK_SECRET=whsec_... +``` + +### Database Schema + +#### User Table (MySQL) +```sql +CREATE TABLE users ( + id VARCHAR(36) PRIMARY KEY, -- Clerk user ID + clerk_id VARCHAR(255) UNIQUE NOT NULL, -- Clerk user ID for reference + email VARCHAR(255) UNIQUE NOT NULL, + username VARCHAR(100) UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- Profile (synced from Clerk) + first_name VARCHAR(100), + last_name VARCHAR(100), + avatar_url VARCHAR(500), + + -- App-specific data + impact_score INT DEFAULT 0, + + -- Preferences + notification_email BOOLEAN DEFAULT TRUE, + notification_in_app BOOLEAN DEFAULT TRUE, + areas_of_interest JSON, + + INDEX idx_clerk_id (clerk_id), + INDEX idx_email (email), + INDEX idx_username (username) +) ENGINE=InnoDB; +``` + +**TypeScript Interface:** +```typescript +interface User { + id: string; // Clerk user ID + clerkId: string; + email: string; + username: string; + createdAt: Date; + updatedAt: Date; + firstName?: string; + lastName?: string; + avatarUrl?: string; + impactScore: number; + notificationEmail: boolean; + notificationInApp: boolean; + areasOfInterest?: Array<{lat: number, lng: number, radius: number}>; +} +``` + +**Note:** User authentication is handled entirely by Clerk. The users table stores only app-specific data and syncs basic profile information from Clerk via webhooks. + +#### Report Table (MySQL) +```sql +CREATE TABLE reports ( + id VARCHAR(36) PRIMARY KEY DEFAULT (UUID()), + user_id VARCHAR(36) NOT NULL, + + -- Location (using POINT spatial type) + location POINT NOT NULL SRID 4326, + latitude DECIMAL(10, 8) NOT NULL, + longitude DECIMAL(11, 8) NOT NULL, + location_source ENUM('exif', 'gps', 'manual') NOT NULL, + + -- Content + photo_urls JSON NOT NULL, + photo_timestamp TIMESTAMP NULL, + litter_type ENUM('plastic', 'metal', 'glass', 'organic', 'hazardous', 'other') NOT NULL, + quantity ENUM('minimal', 'moderate', 'significant', 'severe') NOT NULL, + description TEXT, + + -- Metadata + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- Verification + verification_status ENUM('pending', 'verified', 'disputed') DEFAULT 'pending', + + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + SPATIAL INDEX idx_location (location), + INDEX idx_created_at (created_at), + INDEX idx_litter_type (litter_type), + INDEX idx_lat_lng (latitude, longitude) +) ENGINE=InnoDB; +``` + +**TypeScript Interface:** +```typescript +interface Report { + id: string; + userId: string; + latitude: number; + longitude: number; + locationSource: 'exif' | 'gps' | 'manual'; + photoUrls: string[]; + photoTimestamp?: Date; + litterType: 'plastic' | 'metal' | 'glass' | 'organic' | 'hazardous' | 'other'; + quantity: 'minimal' | 'moderate' | 'significant' | 'severe'; + description?: string; + createdAt: Date; + updatedAt: Date; + submittedAt: Date; + verificationStatus: 'pending' | 'verified' | 'disputed'; +} +``` + +#### Event Table (MySQL) +```sql +CREATE TABLE events ( + id VARCHAR(36) PRIMARY KEY DEFAULT (UUID()), + organizer_id VARCHAR(36) NOT NULL, + + -- Details + title VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + location POINT NOT NULL SRID 4326, + latitude DECIMAL(10, 8) NOT NULL, + longitude DECIMAL(11, 8) NOT NULL, + location_name VARCHAR(255) NOT NULL, + + -- Timing + scheduled_date TIMESTAMP NOT NULL, + duration INT NOT NULL COMMENT 'Duration in minutes', + + -- Status + status ENUM('upcoming', 'completed', 'cancelled') DEFAULT 'upcoming', + participant_count INT DEFAULT 0, + litter_collected DECIMAL(10, 2) COMMENT 'Kilograms collected', + photos JSON, + + -- Metadata + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + FOREIGN KEY (organizer_id) REFERENCES users(id) ON DELETE CASCADE, + SPATIAL INDEX idx_location (location), + INDEX idx_scheduled_date (scheduled_date), + INDEX idx_status (status) +) ENGINE=InnoDB; +``` + +#### EventRegistration Table (MySQL) +```sql +CREATE TABLE event_registrations ( + id VARCHAR(36) PRIMARY KEY DEFAULT (UUID()), + user_id VARCHAR(36) NOT NULL, + event_id VARCHAR(36) NOT NULL, + + registered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + attended BOOLEAN DEFAULT FALSE, + + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE, + UNIQUE KEY unique_user_event (user_id, event_id) +) ENGINE=InnoDB; +``` + +#### Verification Table (MySQL) +```sql +CREATE TABLE verifications ( + id VARCHAR(36) PRIMARY KEY DEFAULT (UUID()), + report_id VARCHAR(36) NOT NULL, + user_id VARCHAR(36) NOT NULL, + + verification_type ENUM('verify', 'dispute') NOT NULL, + comment TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE KEY unique_report_user (report_id, user_id) +) ENGINE=InnoDB; +``` + +#### EnvironmentalConcern Table (MySQL) +```sql +CREATE TABLE environmental_concerns ( + id VARCHAR(36) PRIMARY KEY DEFAULT (UUID()), + report_id VARCHAR(36) NOT NULL, + + concern_type VARCHAR(100) NOT NULL COMMENT 'water_quality, erosion, wildlife_impact, algae_bloom, etc.', + severity ENUM('low', 'medium', 'high', 'critical') NOT NULL, + description TEXT NOT NULL, + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE, + INDEX idx_concern_type (concern_type) +) ENGINE=InnoDB; +``` + +#### Hotspot Table (MySQL) +```sql +CREATE TABLE hotspots ( + id VARCHAR(36) PRIMARY KEY DEFAULT (UUID()), + location POINT NOT NULL SRID 4326, + latitude DECIMAL(10, 8) NOT NULL, + longitude DECIMAL(11, 8) NOT NULL, + radius DECIMAL(10, 2) NOT NULL COMMENT 'Radius in meters', + + report_count INT NOT NULL, + severity_score DECIMAL(5, 2) NOT NULL, + last_report_date TIMESTAMP NOT NULL, + + calculated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + SPATIAL INDEX idx_location (location), + INDEX idx_severity_score (severity_score) +) ENGINE=InnoDB; +``` + +### Data Transfer Objects (DTOs) + +#### CreateReportDTO +```typescript +interface CreateReportDTO { + photos: File[]; + latitude?: number; + longitude?: number; + locationSource: 'exif' | 'gps' | 'manual'; + litterType: LitterType; + quantity: QuantityLevel; + description?: string; + environmentalConcerns?: { + type: string; + severity: string; + description: string; + }[]; +} +``` + +#### ReportResponseDTO +```typescript +interface ReportResponseDTO { + id: string; + user: { + id: string; + username: string; + avatarUrl?: string; + }; + location: { + latitude: number; + longitude: number; + }; + photoUrls: string[]; + photoTimestamp?: string; + litterType: LitterType; + quantity: QuantityLevel; + description?: string; + verificationStatus: VerificationStatus; + verificationCount: number; + disputeCount: number; + createdAt: string; + environmentalConcerns: EnvironmentalConcernDTO[]; +} +``` + +## Error Handling + +### Error Response Format + +All API errors follow a consistent format: + +```typescript +interface ErrorResponse { + error: { + code: string; + message: string; + details?: any; + timestamp: string; + } +} +``` + +### Error Codes + +```typescript +enum ErrorCode { + // Authentication + UNAUTHORIZED = 'UNAUTHORIZED', + INVALID_CREDENTIALS = 'INVALID_CREDENTIALS', + TOKEN_EXPIRED = 'TOKEN_EXPIRED', + + // Validation + VALIDATION_ERROR = 'VALIDATION_ERROR', + INVALID_COORDINATES = 'INVALID_COORDINATES', + INVALID_FILE_TYPE = 'INVALID_FILE_TYPE', + FILE_TOO_LARGE = 'FILE_TOO_LARGE', + + // Resources + RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND', + DUPLICATE_RESOURCE = 'DUPLICATE_RESOURCE', + + // Permissions + FORBIDDEN = 'FORBIDDEN', + INSUFFICIENT_PERMISSIONS = 'INSUFFICIENT_PERMISSIONS', + + // External Services + EXTERNAL_API_ERROR = 'EXTERNAL_API_ERROR', + GEOCODING_FAILED = 'GEOCODING_FAILED', + + // Server + INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', + DATABASE_ERROR = 'DATABASE_ERROR', +} +``` + +### Error Handling Strategy + +1. **Input Validation**: Use Zod schemas for request validation at API boundary +2. **Try-Catch Blocks**: Wrap async operations with proper error handling +3. **Error Middleware**: Global Express error handler for consistent responses +4. **Logging**: Winston logger for error tracking and debugging +5. **User-Friendly Messages**: Transform technical errors into readable messages +6. **Retry Logic**: Implement exponential backoff for external API calls +7. **Circuit Breaker**: Prevent cascading failures from external services + +### Frontend Error Handling + +1. **Error Boundaries**: React error boundaries for component-level errors +2. **Toast Notifications**: User-friendly error messages via toast library +3. **Retry Mechanisms**: Automatic retry for failed API calls +4. **Offline Support**: Queue actions when offline, sync when online +5. **Validation Feedback**: Real-time form validation with clear messages + +## Testing Strategy + +### Unit Testing + +**Backend**: +- Jest for test framework +- Test all service layer functions +- Mock database calls with Prisma mock +- Test utility functions (EXIF extraction, coordinate validation) +- Target: 80% code coverage + +**Frontend**: +- Vitest for test framework +- React Testing Library for component tests +- Test user interactions and state changes +- Mock API calls with MSW (Mock Service Worker) +- Target: 70% code coverage + +### Integration Testing + +- Test API endpoints with Supertest +- Use test database with Docker +- Test authentication flows +- Test file upload pipeline +- Test external API integration with mocked responses + +### End-to-End Testing + +- Playwright or Cypress for E2E tests +- Test critical user journeys: + - User registration and login + - Submit report with photo + - View reports on map + - Create and register for event + - View analytics dashboard +- Run on CI/CD pipeline + +### Performance Testing + +- Load testing with Artillery or k6 +- Test concurrent report submissions +- Test map rendering with 1000+ markers +- Test database query performance with PostGIS +- Monitor API response times + +### Accessibility Testing + +- Axe DevTools for automated a11y testing +- Manual keyboard navigation testing +- Screen reader compatibility (NVDA, JAWS) +- WCAG 2.1 AA compliance target +- Color contrast validation + +## Security Considerations + +### Authentication & Authorization + +- Clerk handles all authentication (sign-up, sign-in, password management, MFA) +- Session tokens verified via Clerk middleware on backend +- User data synced to local database via Clerk webhooks +- Rate limiting on API endpoints +- Clerk provides built-in email verification and social auth options + +### Data Protection + +- HTTPS only in production +- Input sanitization to prevent XSS +- Parameterized queries to prevent SQL injection +- CORS configuration for allowed origins +- Content Security Policy headers + +### File Upload Security + +- File type validation (MIME type checking) +- File size limits (5MB per photo) +- Virus scanning for uploaded files +- Separate storage domain for user content +- Strip EXIF data except GPS/timestamp before public display + +### API Security + +- Rate limiting per IP and per user +- API key rotation for external services +- Secrets management (environment variables) +- Request validation with Zod schemas +- CSRF protection for state-changing operations + +## Performance Optimization + +### Frontend Optimization + +- Code splitting by route +- Lazy loading for map components +- Image optimization and lazy loading +- Virtual scrolling for large lists +- Debouncing for search and filter inputs +- Service worker for offline capability +- CDN for static assets + +### Backend Optimization + +- Database indexing on frequently queried fields +- PostGIS spatial indexes for location queries +- Redis caching for: + - External API responses (6 hours) + - Hotspot calculations (15 minutes) + - Analytics aggregations (1 hour) +- Connection pooling for database +- Pagination for list endpoints +- Compression middleware (gzip) + +### Database Optimization + +- Materialized views for hotspot calculations +- Scheduled jobs to refresh materialized views +- Partitioning for large tables (by date) +- Query optimization with EXPLAIN ANALYZE +- Read replicas for analytics queries + +## Deployment Architecture + +### Development Environment + +- Docker Compose for local development +- PostgreSQL + PostGIS container +- Redis container +- Hot reload for frontend and backend +- Mock external APIs for testing + +### Production Environment + +- Frontend: Vercel or Netlify +- Backend: Railway, Render, or AWS ECS +- Database: Managed PostgreSQL (AWS RDS, Supabase) +- Redis: Managed Redis (AWS ElastiCache, Upstash) +- Storage: AWS S3 or Cloudinary +- CDN: CloudFlare +- Monitoring: Sentry for error tracking +- Analytics: Plausible or Google Analytics + +### CI/CD Pipeline + +1. GitHub Actions workflow +2. Run linting and type checking +3. Run unit and integration tests +4. Build Docker images +5. Deploy to staging environment +6. Run E2E tests on staging +7. Deploy to production (manual approval) + +## Scalability Considerations + +### Horizontal Scaling + +- Stateless API servers (can add more instances) +- Load balancer for API traffic +- Database read replicas for analytics +- CDN for static content and images + +### Data Growth Strategy + +- Archive old reports (>2 years) to cold storage +- Implement data retention policies +- Optimize photo storage with compression +- Use database partitioning for time-series data + +### Future Enhancements + +- Mobile native apps (React Native) +- Real-time updates with WebSockets +- Machine learning for litter type classification from photos +- Gamification with leaderboards and achievements +- Integration with local government systems +- Multi-language support for international expansion +- API for third-party integrations +- Blockchain for verified cleanup certificates diff --git a/.kiro/specs/beach-litter-mapping/requirements.md b/.kiro/specs/beach-litter-mapping/requirements.md new file mode 100644 index 0000000..0e0fa1d --- /dev/null +++ b/.kiro/specs/beach-litter-mapping/requirements.md @@ -0,0 +1,156 @@ +# Requirements Document + +## Introduction + +The Beach Litter Mapping Platform is a web-based application designed to crowdsource environmental data about beach litter and other coastal concerns across Ireland. The system empowers communities to report, visualize, and address environmental issues through interactive mapping, data analytics, and coordinated clean-up events. By providing real-time visibility into coastal pollution patterns, the platform aims to raise awareness, drive community engagement, and support evidence-based environmental action. + +## Glossary + +- **Platform**: The Beach Litter Mapping web application system +- **User**: Any person accessing the Platform, including reporters, volunteers, and viewers +- **Litter Report**: A user-submitted record documenting observed beach litter or environmental concerns +- **Map Interface**: The interactive map component displaying geographical data and litter overlays +- **Clean-up Event**: An organized community activity to remove litter from a specific location +- **Litter Hotspot**: A geographical area with high concentration of reported litter incidents +- **Environmental Concern**: Any coastal pollution or ecological issue beyond physical litter (e.g., water quality, erosion) +- **Report Verification**: The process of confirming the accuracy and validity of submitted reports +- **Analytics Dashboard**: The interface displaying aggregated statistics and trends about litter data + +## Requirements + +### Requirement 1: Litter Reporting System + +**User Story:** As a beach visitor, I want to report litter I observe with photos and location data, so that the community can be aware of pollution issues and take action. + +#### Acceptance Criteria + +1. WHEN a User submits a litter report, THE Platform SHALL capture the geographical coordinates of the report location with accuracy within 10 meters +2. WHEN a User uploads a photograph with EXIF metadata containing GPS coordinates, THE Platform SHALL extract and use those coordinates as the report location +3. WHEN a User uploads a photograph without GPS metadata, THE Platform SHALL prompt the User to provide location through device GPS or manual map selection +4. WHEN a User submits a litter report, THE Platform SHALL accept at least one photograph with minimum resolution of 800x600 pixels +5. WHEN a User uploads a photograph with EXIF metadata containing timestamp, THE Platform SHALL extract and display the original photo capture time +6. THE Platform SHALL allow Users to categorize litter by type (plastic, metal, glass, organic, hazardous, other) +7. THE Platform SHALL allow Users to estimate litter quantity using predefined ranges (minimal, moderate, significant, severe) +8. WHEN a User submits a report, THE Platform SHALL record the submission timestamp in ISO 8601 format + +### Requirement 2: Interactive Map Visualization + +**User Story:** As a community member, I want to view an interactive map showing all reported litter locations, so that I can understand pollution patterns in my area. + +#### Acceptance Criteria + +1. THE Platform SHALL display litter reports as visual markers on an interactive map using free mapping services +2. WHEN a User clicks on a map marker, THE Platform SHALL display the full report details including photos, category, quantity, and timestamp +3. THE Platform SHALL provide map zoom functionality from country-level view to street-level detail +4. THE Platform SHALL allow Users to filter visible reports by date range, litter type, and quantity level +5. WHEN multiple reports exist within 50 meters, THE Platform SHALL cluster markers and display the count of reports in that cluster + +### Requirement 3: Litter Hotspot Analysis + +**User Story:** As an environmental researcher, I want to identify areas with high concentrations of litter reports, so that I can prioritize clean-up efforts and study pollution patterns. + +#### Acceptance Criteria + +1. THE Platform SHALL generate heat map overlays showing litter density across geographical regions +2. WHEN the Platform calculates Litter Hotspots, THE Platform SHALL identify areas with at least 5 reports within a 500-meter radius over the past 30 days +3. THE Platform SHALL rank Litter Hotspots by severity score calculated from report quantity and frequency +4. THE Platform SHALL update Litter Hotspot calculations within 5 minutes of new report submissions +5. THE Platform SHALL display Litter Hotspot boundaries on the Map Interface with color-coded severity indicators + +### Requirement 4: Clean-up Event Management + +**User Story:** As a community organizer, I want to create and manage clean-up events at reported litter locations, so that volunteers can coordinate their efforts effectively. + +#### Acceptance Criteria + +1. THE Platform SHALL allow Users to create Clean-up Events with specified date, time, location, and description +2. WHEN a User creates a Clean-up Event, THE Platform SHALL display the event marker on the Map Interface +3. THE Platform SHALL allow Users to register their participation in Clean-up Events +4. THE Platform SHALL display the count of registered participants for each Clean-up Event +5. WHEN a Clean-up Event date passes, THE Platform SHALL allow the organizer to mark the event as completed and report the quantity of litter collected + +### Requirement 5: Analytics and Reporting Dashboard + +**User Story:** As an environmental advocate, I want to view statistics and trends about beach litter, so that I can use data to support awareness campaigns and policy recommendations. + +#### Acceptance Criteria + +1. THE Platform SHALL display total count of litter reports aggregated by time period (daily, weekly, monthly, yearly) +2. THE Platform SHALL generate charts showing litter distribution by category type +3. THE Platform SHALL calculate and display the total estimated quantity of litter reported across all locations +4. THE Platform SHALL show trends comparing current period data to previous periods with percentage change indicators +5. THE Platform SHALL allow Users to export analytics data in CSV format for external analysis + +### Requirement 6: User Authentication and Profiles + +**User Story:** As a platform user, I want to create an account and track my contributions, so that I can build a history of my environmental impact. + +#### Acceptance Criteria + +1. THE Platform SHALL allow Users to create accounts using email address and password with minimum 8 characters +2. THE Platform SHALL authenticate Users before allowing report submissions or event creation +3. THE Platform SHALL display a User profile showing their submitted reports count and Clean-up Events attended +4. THE Platform SHALL calculate and display an impact score for each User based on their contributions +5. WHEN a User views their profile, THE Platform SHALL show a map of all their submitted report locations + +### Requirement 7: Report Verification and Quality Control + +**User Story:** As a platform administrator, I want to verify submitted reports to maintain data quality, so that the platform provides reliable information to the community. + +#### Acceptance Criteria + +1. THE Platform SHALL flag reports for review when they lack photographs or location data +2. THE Platform SHALL allow designated Users to mark reports as verified or disputed +3. WHEN a report receives 3 or more dispute flags, THE Platform SHALL hide the report from public view pending review +4. THE Platform SHALL display verification status indicators on report details +5. THE Platform SHALL maintain an audit log of all verification actions with User identifier and timestamp + +### Requirement 8: Mobile Responsiveness and Accessibility + +**User Story:** As a mobile user at the beach, I want to easily report litter from my smartphone, so that I can contribute data while observing environmental issues in real-time. + +#### Acceptance Criteria + +1. THE Platform SHALL render all core functionality on mobile devices with screen widths from 320 pixels to 768 pixels +2. THE Platform SHALL access device GPS capabilities to auto-populate report location coordinates +3. THE Platform SHALL access device camera to capture photos directly within the reporting interface +4. THE Platform SHALL maintain touch-friendly interface elements with minimum tap target size of 44x44 pixels +5. THE Platform SHALL load the Map Interface within 3 seconds on mobile networks with 4G connectivity + +### Requirement 9: Environmental Data Integration + +**User Story:** As a scientist, I want to correlate litter data with other environmental factors, so that I can conduct comprehensive coastal health assessments. + +#### Acceptance Criteria + +1. THE Platform SHALL allow Users to report additional Environmental Concerns beyond physical litter (water quality, wildlife impact, coastal erosion) +2. THE Platform SHALL categorize Environmental Concerns using standardized taxonomy +3. THE Platform SHALL display Environmental Concern reports as distinct markers on the Map Interface +4. THE Platform SHALL link related reports when they reference the same geographical area within 100 meters +5. THE Platform SHALL provide API endpoints to export environmental data in JSON format for integration with research tools + +### Requirement 10: Notification and Engagement System + +**User Story:** As an active volunteer, I want to receive notifications about new reports and events in my area, so that I can respond quickly to environmental issues. + +#### Acceptance Criteria + +1. THE Platform SHALL allow Users to define geographical areas of interest with radius between 1 and 50 kilometers +2. WHEN a new Litter Report is submitted within a User's area of interest, THE Platform SHALL send a notification to that User within 10 minutes +3. WHEN a new Clean-up Event is created within a User's area of interest, THE Platform SHALL send a notification to that User within 10 minutes +4. THE Platform SHALL allow Users to configure notification preferences for report types and event categories +5. THE Platform SHALL deliver notifications through in-app alerts and email messages based on User preferences + +### Requirement 11: External Environmental Data Integration + +**User Story:** As a researcher, I want to view contextual environmental data alongside litter reports, so that I can understand the broader ecological conditions affecting coastal areas. + +#### Acceptance Criteria + +1. THE Platform SHALL integrate weather data from public APIs to display current conditions and forecasts for reported locations +2. THE Platform SHALL retrieve tide information from public APIs to show tide times and heights for coastal locations +3. THE Platform SHALL integrate water quality data from Irish Environmental Protection Agency (EPA) public datasets where available +4. WHEN a User views a report location, THE Platform SHALL display relevant environmental context data within 2 seconds +5. THE Platform SHALL cache external API data for up to 6 hours to minimize API calls and improve performance +6. THE Platform SHALL integrate beach quality ratings from EU Blue Flag or similar public databases for Irish beaches +7. THE Platform SHALL display marine biodiversity information from public databases when available for the report location diff --git a/.kiro/specs/beach-litter-mapping/tasks.md b/.kiro/specs/beach-litter-mapping/tasks.md new file mode 100644 index 0000000..bd956cb --- /dev/null +++ b/.kiro/specs/beach-litter-mapping/tasks.md @@ -0,0 +1,323 @@ +# Implementation Plan + +## Frontend Foundation + +- [x] 1. Set up project structure and dependencies + - Install required frontend dependencies: Leaflet, React Query, Tailwind CSS, React Router, date-fns, chart libraries + - Configure Tailwind CSS with Vite + - Set up folder structure: components, pages, services, hooks, utils, types + - Create TypeScript configuration for type safety + - _Requirements: 8.1, 8.4_ + +- [x] 2. Implement authentication UI and state management + - Create login and registration forms with validation + - Implement JWT token storage and refresh logic + - Create protected route wrapper component + - Build authentication context provider + - Create user profile display component + - _Requirements: 6.1, 6.2, 6.3_ + +- [x] 3. Build interactive map component with Leaflet + - Initialize Leaflet map with OpenStreetMap tiles + - Implement map zoom and pan controls + - Add geolocation button to center map on user location + - Create custom marker icons for different litter types + - Implement marker click handlers to show report details + - _Requirements: 2.1, 2.2, 2.3, 8.2_ + +- [x] 4. Implement marker clustering and filtering + - Integrate Leaflet.markercluster plugin + - Configure cluster radius and appearance + - Build filter controls for date range, litter type, and quantity + - Implement filter state management and map updates + - _Requirements: 2.4, 2.5_ + +- [ ] 5. Create heat map visualization + - Integrate Leaflet.heat plugin + - Implement heat map toggle control + - Configure heat map intensity based on report density + - Add heat map layer management + - _Requirements: 3.1, 3.5_ + +## Report Submission System + +- [ ] 6. Build photo upload component with EXIF extraction + - Create drag-and-drop photo upload interface + - Implement file type and size validation + - Integrate exifr library to extract GPS coordinates and timestamps + - Display photo previews with metadata + - Handle multiple photo uploads + - _Requirements: 1.2, 1.3, 1.4, 1.5, 8.3_ + +- [ ] 7. Implement location selection interface + - Auto-populate location from EXIF data when available + - Add device GPS location capture button + - Create map-based location picker for manual selection + - Display selected location coordinates with accuracy indicator + - Validate coordinate ranges for Ireland + - _Requirements: 1.1, 1.2, 1.3, 8.2_ + +- [ ] 8. Create litter report form + - Build multi-step form with progress indicator + - Implement litter type categorization dropdown + - Create quantity estimation slider with visual indicators + - Add optional description textarea + - Implement form validation with error messages + - Create environmental concerns section + - _Requirements: 1.6, 1.7, 9.1, 9.2_ + +- [ ] 9. Implement report submission and display + - Connect form to API endpoint for report creation + - Handle submission success and error states + - Create report detail modal/popup component + - Display report information with photos and metadata + - Show verification status indicators + - _Requirements: 1.8, 7.4_ + +## Analytics and Visualization + +- [ ] 10. Build analytics dashboard layout + - Create dashboard page with responsive grid layout + - Implement summary statistics cards (total reports, events, users) + - Add date range selector component + - Create export to CSV functionality + - _Requirements: 5.1, 5.5_ + +- [ ] 11. Implement data visualization charts + - Create time series chart for report trends + - Build pie chart for litter type distribution + - Implement comparison metrics with previous periods + - Add quantity estimation aggregation display + - Configure chart responsiveness for mobile + - _Requirements: 5.2, 5.3, 5.4_ + +- [ ] 12. Create hotspot visualization + - Fetch and display hotspot data on map + - Implement color-coded severity indicators + - Show hotspot boundaries and statistics + - Create hotspot detail popup with report count + - _Requirements: 3.2, 3.3, 3.5_ + +## Event Management + +- [ ] 13. Build event creation and management interface + - Create event creation form with date/time picker + - Implement location selection on map + - Add participant registration functionality + - Build event list view with filters + - Create event detail page with participant count + - _Requirements: 4.1, 4.2, 4.3, 4.4_ + +- [ ] 14. Implement event completion workflow + - Create event completion form for organizers + - Add litter collected quantity input + - Implement photo upload for cleanup results + - Update event status and display completion data + - _Requirements: 4.5_ + +## User Profile and Engagement + +- [ ] 15. Build user profile page + - Display user statistics (reports, events attended) + - Calculate and show impact score + - Create personal contribution map + - Implement activity timeline + - Add profile editing functionality + - _Requirements: 6.3, 6.4, 6.5_ + +- [ ] 16. Implement notification preferences + - Create notification settings interface + - Build area of interest selector with map + - Implement radius configuration (1-50km) + - Add notification type toggles (email, in-app) + - Save and load user preferences + - _Requirements: 10.1, 10.4_ + +## Report Verification System + +- [ ] 17. Implement report verification interface + - Add verify/dispute buttons to report details + - Create verification comment form + - Display verification count and status + - Implement auto-hide logic for disputed reports (3+ disputes) + - Show verification audit log + - _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5_ + +## External Data Integration + +- [ ] 18. Create external data display components + - Build weather information widget + - Implement tide times display + - Add water quality indicator + - Create beach quality rating display + - Show marine biodiversity information when available + - _Requirements: 11.1, 11.2, 11.3, 11.4, 11.6, 11.7_ + +## Mobile Optimization + +- [ ] 19. Implement mobile-responsive design + - Apply Tailwind responsive classes throughout + - Optimize map controls for touch interfaces + - Ensure minimum tap target sizes (44x44px) + - Test and optimize for screen widths 320px-768px + - Implement mobile navigation menu + - _Requirements: 8.1, 8.4_ + +- [ ] 20. Optimize mobile performance + - Implement code splitting and lazy loading + - Add image optimization and lazy loading + - Configure service worker for offline capability + - Optimize bundle size and load times + - Test on 4G network conditions + - _Requirements: 8.5_ + +## Backend API Development + +- [ ] 21. Set up backend project structure + - Initialize Node.js project with Express + - Configure TypeScript for backend + - Set up folder structure: routes, controllers, services, middleware, utils + - Install dependencies: Prisma, JWT, Multer, bcrypt, express-validator + - Create environment configuration + - _Requirements: All backend requirements_ + +- [ ] 22. Configure database with Prisma and PostGIS + - Set up PostgreSQL with PostGIS extension + - Create Prisma schema with all models (User, Report, Event, etc.) + - Configure PostGIS geography types for location fields + - Create database migrations + - Set up database indexes for performance + - _Requirements: 1.1, 2.1, 3.2, 4.1_ + +- [ ] 23. Implement authentication system + - Create user registration endpoint with password hashing + - Build login endpoint with JWT token generation + - Implement refresh token logic with httpOnly cookies + - Create authentication middleware for protected routes + - Add rate limiting to auth endpoints + - _Requirements: 6.1, 6.2_ + +- [ ] 24. Build report API endpoints + - Create POST /api/reports endpoint for report submission + - Implement GET /api/reports with filtering and pagination + - Build GET /api/reports/:id for single report retrieval + - Create PATCH /api/reports/:id for updates + - Implement DELETE /api/reports/:id with authorization + - Add spatial queries using PostGIS for location-based filtering + - _Requirements: 1.1-1.8, 2.1, 2.4_ + +- [ ] 25. Implement file upload and EXIF processing + - Configure Multer for photo uploads + - Set up cloud storage (S3 or Cloudinary) integration + - Implement EXIF metadata extraction with exifr + - Add file validation (type, size limits) + - Create photo URL generation and storage + - _Requirements: 1.2, 1.3, 1.4, 1.5_ + +- [ ] 26. Build event management endpoints + - Create POST /api/events for event creation + - Implement GET /api/events with filtering + - Build event registration endpoint + - Create event completion endpoint + - Add participant count tracking + - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_ + +- [ ] 27. Implement analytics endpoints + - Create GET /api/analytics/summary for overall statistics + - Build time series data endpoint for trends + - Implement CSV export functionality + - Add aggregation queries for litter types and quantities + - Create comparison calculations for period-over-period metrics + - _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5_ + +- [ ] 28. Build hotspot calculation system + - Create scheduled job with node-cron for hotspot calculations + - Implement PostGIS spatial queries to identify clusters + - Calculate severity scores based on report count and frequency + - Store hotspot data in database or materialized view + - Create GET /api/analytics/hotspots endpoint + - _Requirements: 3.2, 3.3, 3.4_ + +- [ ] 29. Implement verification system endpoints + - Create POST /api/reports/:id/verify endpoint + - Build POST /api/reports/:id/dispute endpoint + - Implement verification count tracking + - Add auto-hide logic for reports with 3+ disputes + - Create verification audit log + - _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5_ + +- [ ] 30. Build user profile endpoints + - Create GET /api/users/:id for profile retrieval + - Implement PATCH /api/users/:id for profile updates + - Build GET /api/users/:id/reports endpoint + - Create GET /api/users/:id/events endpoint + - Implement impact score calculation + - Add notification preferences endpoint + - _Requirements: 6.3, 6.4, 6.5, 10.4_ + +- [ ] 31. Integrate external APIs with caching + - Set up Redis for caching external API responses + - Implement weather API integration (OpenWeatherMap) + - Build tide API integration (WorldTides or NOAA) + - Create EPA water quality data integration + - Add beach quality rating integration + - Implement 6-hour cache expiration + - _Requirements: 11.1, 11.2, 11.3, 11.4, 11.5, 11.6_ + +- [ ] 32. Implement notification system + - Create notification service with area-of-interest matching + - Build email notification integration (SendGrid) + - Implement in-app notification storage and retrieval + - Create scheduled job to check for new reports/events + - Add notification delivery within 10-minute requirement + - _Requirements: 10.1, 10.2, 10.3, 10.4_ + +- [ ] 33. Add API security and validation + - Implement request validation with Zod schemas + - Add rate limiting middleware + - Configure CORS for allowed origins + - Implement input sanitization + - Add security headers (helmet) + - Create error handling middleware + - _Requirements: All security-related requirements_ + +- [ ] 34. Implement environmental concerns endpoints + - Create endpoints for submitting environmental concerns + - Build filtering and retrieval for concern types + - Implement linking of related reports within 100m + - Add JSON export endpoint for research integration + - _Requirements: 9.1, 9.2, 9.3, 9.4, 9.5_ + +## Integration and Polish + +- [ ] 35. Connect frontend to backend APIs + - Configure API base URL and axios/fetch setup + - Implement React Query hooks for all endpoints + - Add error handling and retry logic + - Create loading states for all async operations + - Implement optimistic updates where appropriate + - _Requirements: All integration requirements_ + +- [ ] 36. Implement error handling and user feedback + - Create toast notification system for user feedback + - Add error boundaries for React components + - Implement form validation error displays + - Create user-friendly error messages + - Add loading spinners and skeleton screens + - _Requirements: All UX requirements_ + +- [ ]* 37. Write integration tests + - Create API endpoint tests with Supertest + - Test authentication flows + - Test file upload pipeline + - Test spatial queries with PostGIS + - Test external API integration with mocks + - _Requirements: All requirements_ + +- [ ] 38. Set up deployment configuration + - Create Docker configuration for development + - Set up environment variables for production + - Configure build scripts for frontend and backend + - Create database migration scripts + - Document deployment process + - _Requirements: Infrastructure requirements_ diff --git a/.kiro/steering/product.md b/.kiro/steering/product.md new file mode 100644 index 0000000..0f4034f --- /dev/null +++ b/.kiro/steering/product.md @@ -0,0 +1,26 @@ +# Product Overview + +The Beach Litter Mapping Platform is a crowdsourced environmental monitoring web application for Irish coastal areas. It enables users to report beach litter with photos and GPS coordinates, visualize pollution patterns on interactive maps, organize cleanup events, and analyze environmental data. + +## Core Features + +- **Litter Reporting**: Submit reports with photos, GPS location, litter type categorization, and quantity estimates +- **Interactive Mapping**: View reports on Leaflet maps with clustering, heat maps, and filtering by date/type/quantity +- **Hotspot Analysis**: Identify high-concentration litter areas (5+ reports within 500m radius) +- **Event Management**: Create and coordinate community cleanup events with participant registration +- **Analytics Dashboard**: Visualize trends, statistics, and export data for research +- **User Profiles**: Track contributions, impact scores, and personal report history +- **Verification System**: Community-driven report verification and quality control +- **Environmental Context**: Integrate weather, tide, and water quality data from public APIs + +## Target Users + +- Beach visitors reporting litter +- Community organizers coordinating cleanups +- Environmental researchers analyzing pollution patterns +- Volunteers responding to local issues +- Policy advocates using data for campaigns + +## Geographic Scope + +Primary focus on Irish coastal areas with coordinate validation for Irish boundaries. diff --git a/.kiro/steering/structure.md b/.kiro/steering/structure.md new file mode 100644 index 0000000..7911955 --- /dev/null +++ b/.kiro/steering/structure.md @@ -0,0 +1,119 @@ +# Project Structure + +## Directory Organization + +``` +src/ +├── components/ # React components organized by feature +│ ├── auth/ # Authentication components (SignIn, SignUp, ProtectedRoute) +│ ├── layout/ # Layout components (Header, Footer, Navigation) +│ └── map/ # Map components (MapView, MapFilters) +├── pages/ # Page-level components for routing +├── services/ # API clients and external service integrations +├── hooks/ # Custom React hooks +├── utils/ # Utility functions and helpers +├── types/ # TypeScript type definitions +├── App.tsx # Main application component with routing +├── main.tsx # Application entry point +└── index.css # Global styles with Tailwind directives +``` + +## Component Organization + +Components are organized by **feature/domain** rather than by type: + +- `components/auth/` - All authentication-related components +- `components/map/` - All mapping-related components +- `components/layout/` - Shared layout components +- Each feature folder includes an `index.ts` for clean exports + +## File Naming Conventions + +- **Components**: PascalCase (e.g., `MapView.tsx`, `ReportForm.tsx`) +- **Hooks**: camelCase with `use` prefix (e.g., `useAuth.ts`, `useGeolocation.ts`) +- **Services**: camelCase (e.g., `api.ts`, `reportService.ts`) +- **Utils**: camelCase (e.g., `validation.ts`, `coordinates.ts`) +- **Types**: PascalCase for interfaces/types in `types/index.ts` + +## Import/Export Patterns + +Each feature directory includes an `index.ts` barrel file for clean exports: + +```typescript +// components/map/index.ts +export { MapView } from './MapView'; +export { MapFilters } from './MapFilters'; + +// Usage elsewhere +import { MapView, MapFilters } from '@/components/map'; +``` + +## Page Structure + +Pages in `src/pages/` correspond to routes: + +- `HomePage.tsx` - Landing page +- `MapPage.tsx` - Interactive map view +- `ReportFormPage.tsx` - Report submission form +- `EventsPage.tsx` - Event listing and management +- `AnalyticsPage.tsx` - Analytics dashboard + +All pages are exported through `pages/index.ts` for centralized route configuration. + +## Type Definitions + +Centralized in `src/types/index.ts`: + +- Domain models (Report, Event, User) +- DTOs (Data Transfer Objects) +- Enums (LitterType, QuantityLevel, VerificationStatus) +- API response types + +## Service Layer + +Services in `src/services/` handle external communication: + +- `api.ts` - Base API client with HTTP methods +- `reportService.ts` - Report-related API calls +- `eventService.ts` - Event-related API calls +- `analyticsService.ts` - Analytics API calls +- `exifService.ts` - EXIF metadata extraction + +## Hooks + +Custom hooks in `src/hooks/`: + +- `useAuth.ts` - Authentication state and methods (Clerk wrapper) +- `useGeolocation.ts` - Device geolocation access +- Additional hooks as needed for shared logic + +## Utilities + +Helper functions in `src/utils/`: + +- Coordinate validation +- Distance calculations +- File validation +- Date formatting +- Other pure utility functions + +## Styling Approach + +- **Tailwind CSS** for component styling +- Utility classes directly in JSX +- Global styles in `index.css` (minimal) +- No CSS modules or styled-components + +## Authentication Pattern + +- Clerk handles all authentication +- Use `` and `` components for conditional rendering +- Use `` wrapper for authenticated pages +- Access user with `useUser()` hook from Clerk + +## State Management + +- **Local state**: React useState for component-specific state +- **Server state**: React Query for API data fetching/caching +- **Auth state**: Clerk provider +- No Redux or other global state libraries diff --git a/.kiro/steering/tech.md b/.kiro/steering/tech.md new file mode 100644 index 0000000..fb92d70 --- /dev/null +++ b/.kiro/steering/tech.md @@ -0,0 +1,93 @@ +# Technology Stack + +## Frontend + +- **React 19** with **TypeScript** for type-safe development +- **Vite** for build tooling and dev server +- **React Router** for client-side routing +- **Tailwind CSS v4** for styling (utility-first) +- **Clerk** (@clerk/clerk-react) for authentication +- **React Query** (@tanstack/react-query) for server state management + +### Mapping & Visualization + +- **Leaflet** for interactive maps (free, open-source) +- **OpenStreetMap** tiles as base layer +- **Leaflet.heat** for heat map overlays +- **Leaflet.markercluster** for marker clustering +- **Recharts** for charts and data visualization + +### Utilities + +- **date-fns** for date manipulation + +## Backend (Planned) + +- **Node.js** with **Express.js** REST API +- **MySQL 8.0+** database with spatial data support +- **Prisma** ORM for database access +- **Clerk** for authentication (backend SDK) +- **Multer** for file uploads +- **exifr** for EXIF metadata extraction +- **Redis** for caching external API responses + +## Path Aliases + +Use TypeScript path aliases for cleaner imports: + +```typescript +import { Report } from '@/types'; +import { apiClient } from '@/services/api'; +import { useGeolocation } from '@/hooks'; +``` + +Available aliases: +- `@/*` → `src/*` +- `@/components/*` → `src/components/*` +- `@/pages/*` → `src/pages/*` +- `@/services/*` → `src/services/*` +- `@/hooks/*` → `src/hooks/*` +- `@/utils/*` → `src/utils/*` +- `@/types/*` → `src/types/*` + +## Environment Variables + +Required in `.env.local`: + +```env +# Clerk Authentication +VITE_CLERK_PUBLISHABLE_KEY=your_publishable_key + +# API Configuration +VITE_API_BASE_URL=http://localhost:3000/api +``` + +## Common Commands + +```bash +# Development +npm run dev # Start dev server (http://localhost:5173) + +# Build +npm run build # Production build to dist/ + +# Preview +npm run preview # Preview production build locally + +# Linting +npm run lint # Run ESLint +``` + +## TypeScript Configuration + +- **Strict mode** enabled +- **noUnusedLocals** and **noUnusedParameters** enforced +- **noUncheckedIndexedAccess** for safer array/object access +- **JSX**: react-jsx (React 17+ transform) +- **Module resolution**: bundler mode + +## Code Quality + +- ESLint with React hooks and React refresh plugins +- Strict TypeScript checking +- No unused variables/parameters allowed diff --git a/README.md b/README.md new file mode 100644 index 0000000..18bc70e --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@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 + +## React Compiler + +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). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..4fa125d --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/index.html b/index.html new file mode 100644 index 0000000..6ac2528 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Beach Litter Mapping Platform + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b4f0f6b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4217 @@ +{ + "name": "ocean-cleanup", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ocean-cleanup", + "version": "0.0.0", + "dependencies": { + "@clerk/clerk-react": "^5.55.0", + "@tanstack/react-query": "^5.90.9", + "date-fns": "^4.1.0", + "leaflet": "^1.9.4", + "leaflet.heat": "^0.2.0", + "leaflet.markercluster": "^1.5.3", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.9.6", + "recharts": "^3.4.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@tailwindcss/postcss": "^4.1.17", + "@types/leaflet": "^1.9.21", + "@types/leaflet.heat": "^0.2.5", + "@types/leaflet.markercluster": "^1.5.6", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.1.0", + "autoprefixer": "^10.4.22", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.17", + "vite": "^7.2.2" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@clerk/clerk-react": { + "version": "5.55.0", + "resolved": "https://registry.npmjs.org/@clerk/clerk-react/-/clerk-react-5.55.0.tgz", + "integrity": "sha512-CWLV0f87utJY2tS40n7imCPJHP2+DPwhoZhCRS2dtYd/c29kLAR5rgydBAy4KkvzZLCPG3Hyt4Vlw1EuMLTq1A==", + "license": "MIT", + "dependencies": { + "@clerk/shared": "^3.34.0", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=18.17.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" + } + }, + "node_modules/@clerk/shared": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-3.34.0.tgz", + "integrity": "sha512-ZuruC+VtvejdnNj+e1sI5JK8tRdOc9wsMWyLNeumXrsZGh92DHTT+FGxCSDPJcmro0iIYw8ZiZqjHT9Fb2gMTg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "csstype": "3.1.3", + "dequal": "2.0.3", + "glob-to-regexp": "0.4.1", + "js-cookie": "3.0.5", + "std-env": "^3.9.0", + "swr": "2.3.4" + }, + "engines": { + "node": ">=18.17.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@clerk/shared/node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.1.tgz", + "integrity": "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.2.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", + "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", + "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-x64": "4.1.17", + "@tailwindcss/oxide-freebsd-x64": "4.1.17", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-x64-musl": "4.1.17", + "@tailwindcss/oxide-wasm32-wasi": "4.1.17", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", + "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", + "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", + "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", + "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", + "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", + "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", + "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", + "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", + "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", + "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.6.0", + "@emnapi/runtime": "^1.6.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", + "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", + "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz", + "integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.17", + "@tailwindcss/oxide": "4.1.17", + "postcss": "^8.4.41", + "tailwindcss": "4.1.17" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.9", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.9.tgz", + "integrity": "sha512-UFOCQzi6pRGeVTVlPNwNdnAvT35zugcIydqjvFUzG62dvz2iVjElmNp/hJkUoM5eqbUPfSU/GJIr/wbvD8bTUw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.9.tgz", + "integrity": "sha512-Zke2AaXiaSfnG8jqPZR52m8SsclKT2d9//AgE/QIzyNvbpj/Q2ln+FsZjb1j69bJZUouBvX2tg9PHirkTm8arw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/leaflet": { + "version": "1.9.21", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/leaflet.heat": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@types/leaflet.heat/-/leaflet.heat-0.2.5.tgz", + "integrity": "sha512-wmDePJPn2vhW9BScsWTV4pSfWayhdDwXuWIQzUuaJkvk5DCHsYsIegTD7tybo38E/0mxpuWODKV/LhsxPESWJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/leaflet": "^1.9" + } + }, + "node_modules/@types/leaflet.markercluster": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/leaflet.markercluster/-/leaflet.markercluster-1.5.6.tgz", + "integrity": "sha512-I7hZjO2+isVXGYWzKxBp8PsCzAYCJBc29qBdFpquOCkS7zFDqUsUvkEOyQHedsk/Cy5tocQzf+Ndorm5W9YKTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/leaflet": "^1.9" + } + }, + "node_modules/@types/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz", + "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", + "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.47", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz", + "integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001755", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", + "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.2.tgz", + "integrity": "sha512-D80T+tiqkd/8B0xNlbstWDG4x6aqVfO52+OlSUNIdkTvmNw0uQpJLeos2J/2XvpyidAFuTPmpad+tUxLndwj6g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "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==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.254", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz", + "integrity": "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-toolkit": { + "version": "1.41.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.41.0.tgz", + "integrity": "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, + "node_modules/leaflet.heat": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/leaflet.heat/-/leaflet.heat-0.2.0.tgz", + "integrity": "sha512-Cd5PbAA/rX3X3XKxfDoUGi9qp78FyhWYurFg3nsfhntcM/MCNK08pRkf4iEenO1KNqwVPKCmkyktjW3UD+h9bQ==" + }, + "node_modules/leaflet.markercluster": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz", + "integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==", + "license": "MIT", + "peerDependencies": { + "leaflet": "^1.3.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/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/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-is": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz", + "integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.6.tgz", + "integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.6" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/recharts": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.4.1.tgz", + "integrity": "sha512-35kYg6JoOgwq8sE4rhYkVWwa6aAIgOtT+Ob0gitnShjwUwZmhrmy7Jco/5kJNF4PnLXgt9Hwq+geEMS+WrjU1g==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/swr": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.4.tgz", + "integrity": "sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f14373d --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "ocean-cleanup", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@clerk/clerk-react": "^5.55.0", + "@tanstack/react-query": "^5.90.9", + "date-fns": "^4.1.0", + "leaflet": "^1.9.4", + "leaflet.heat": "^0.2.0", + "leaflet.markercluster": "^1.5.3", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.9.6", + "recharts": "^3.4.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@tailwindcss/postcss": "^4.1.17", + "@types/leaflet": "^1.9.21", + "@types/leaflet.heat": "^0.2.5", + "@types/leaflet.markercluster": "^1.5.6", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.1.0", + "autoprefixer": "^10.4.22", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.17", + "vite": "^7.2.2" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..1c87846 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + }, +} diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..e583ea2 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,104 @@ +import { ClerkProvider } from '@clerk/clerk-react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; + +// Import Leaflet CSS +import 'leaflet/dist/leaflet.css'; + +// Layout +import { Layout } from './components/layout'; + +// Auth components +import { ProtectedRoute, SignInPage, SignUpPage } from './components/auth'; + +// Pages +import { + HomePage, + MapPage, + ReportFormPage, + EventsPage, + AnalyticsPage +} from './pages'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, // 5 minutes + retry: 1, + }, + }, +}); + +const clerkPubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY; + +if (!clerkPubKey) { + throw new Error('Missing Clerk Publishable Key'); +} + +function App() { + return ( + + + + + {/* Public routes */} + } /> + } /> + + {/* Routes with layout */} + + + + } + /> + + + + } + /> + + {/* Protected routes */} + + + + +
+ } + /> + + + + + + } + /> + + + + + + } + /> + + + +
+ ); +} + +export default App; diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..aaff9c5 --- /dev/null +++ b/src/README.md @@ -0,0 +1,139 @@ +# Beach Litter Mapping Platform - Source Code + +This directory contains the frontend source code for the Beach Litter Mapping Platform. + +## Project Structure + +``` +src/ +├── components/ # React components organized by feature +├── pages/ # Page-level components for routing +├── services/ # API clients and external service integrations +├── hooks/ # Custom React hooks +├── utils/ # Utility functions and helpers +├── types/ # TypeScript type definitions +├── App.tsx # Main application component +├── main.tsx # Application entry point +└── index.css # Global styles with Tailwind CSS +``` + +## Technology Stack + +### Core +- **React 19** - UI library +- **TypeScript** - Type safety +- **Vite** - Build tool and dev server + +### Routing & State +- **React Router** - Client-side routing +- **React Query (@tanstack/react-query)** - Server state management + +### Authentication +- **Clerk** - User authentication and management + +### Mapping +- **Leaflet** - Interactive maps +- **OpenStreetMap** - Free map tiles +- **Leaflet.heat** - Heat map visualization +- **Leaflet.markercluster** - Marker clustering + +### Styling +- **Tailwind CSS v4** - Utility-first CSS framework + +### Data Visualization +- **Recharts** - Charts and graphs + +### Utilities +- **date-fns** - Date manipulation + +## Path Aliases + +The project uses TypeScript path aliases for cleaner imports: + +```typescript +import { Report } from '@/types'; +import { apiClient } from '@/services/api'; +import { useGeolocation } from '@/hooks'; +import { isValidIrishCoordinates } from '@/utils'; +``` + +Available aliases: +- `@/*` → `src/*` +- `@/components/*` → `src/components/*` +- `@/pages/*` → `src/pages/*` +- `@/services/*` → `src/services/*` +- `@/hooks/*` → `src/hooks/*` +- `@/utils/*` → `src/utils/*` +- `@/types/*` → `src/types/*` + +## Environment Variables + +Required environment variables (see `.env.local`): + +```env +# Clerk Authentication +VITE_CLERK_PUBLISHABLE_KEY=your_publishable_key + +# API Configuration +VITE_API_BASE_URL=http://localhost:3000/api +``` + +## Development + +```bash +# Install dependencies +npm install + +# Start development server +npm run dev + +# Build for production +npm run build + +# Preview production build +npm run preview + +# Lint code +npm run lint +``` + +## Key Files + +### `main.tsx` +Application entry point that renders the root component. + +### `App.tsx` +Main application component that sets up: +- Clerk authentication provider +- React Query client +- React Router +- Global layout + +### `types/index.ts` +Central type definitions for: +- Domain models (Report, Event, User, etc.) +- DTOs (Data Transfer Objects) +- Enums (LitterType, QuantityLevel, etc.) + +### `services/api.ts` +Base API client with methods for HTTP requests. + +### `hooks/useGeolocation.ts` +Custom hook for accessing device geolocation. + +### `utils/index.ts` +Utility functions for: +- Coordinate validation +- Distance calculations +- File validation +- Date formatting + +## Next Steps + +The following components and features will be implemented in subsequent tasks: +1. Map interface with Leaflet +2. Report submission form +3. Analytics dashboard +4. Event management +5. User profile +6. Verification system diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/README.md b/src/components/README.md new file mode 100644 index 0000000..3300477 --- /dev/null +++ b/src/components/README.md @@ -0,0 +1,13 @@ +# Components Directory + +This directory contains all React components for the Beach Litter Mapping Platform. + +## Structure + +Components will be organized by feature: +- `map/` - Map-related components (MapInterface, markers, clusters, etc.) +- `reports/` - Report-related components (ReportForm, ReportDetail, etc.) +- `events/` - Event management components +- `analytics/` - Analytics and dashboard components +- `user/` - User profile and authentication components +- `common/` - Shared/reusable components (buttons, modals, forms, etc.) diff --git a/src/components/auth/ProtectedRoute.tsx b/src/components/auth/ProtectedRoute.tsx new file mode 100644 index 0000000..54f422c --- /dev/null +++ b/src/components/auth/ProtectedRoute.tsx @@ -0,0 +1,36 @@ +import { useAuth } from '@clerk/clerk-react'; +import { Navigate, useLocation } from 'react-router-dom'; +import { ReactNode } from 'react'; + +interface ProtectedRouteProps { + children: ReactNode; + redirectTo?: string; +} + +/** + * Protected route wrapper that requires authentication + * Redirects to sign-in page if user is not authenticated + */ +export function ProtectedRoute({ + children, + redirectTo = '/sign-in' +}: ProtectedRouteProps) { + const { isLoaded, isSignedIn } = useAuth(); + const location = useLocation(); + + // Wait for Clerk to load + if (!isLoaded) { + return ( +
+
+
+ ); + } + + // Redirect to sign-in if not authenticated + if (!isSignedIn) { + return ; + } + + return <>{children}; +} diff --git a/src/components/auth/SignInPage.tsx b/src/components/auth/SignInPage.tsx new file mode 100644 index 0000000..7d340ab --- /dev/null +++ b/src/components/auth/SignInPage.tsx @@ -0,0 +1,43 @@ +import { SignIn } from '@clerk/clerk-react'; +import { useNavigate, useLocation } from 'react-router-dom'; + +/** + * Sign-in page with Clerk authentication + */ +export function SignInPage() { + const navigate = useNavigate(); + const location = useLocation(); + + // Get the redirect path from location state, default to home + const from = (location.state as any)?.from?.pathname || '/'; + + return ( +
+
+

+ Sign in to Beach Litter Mapping +

+

+ Join the community in tracking and cleaning coastal pollution +

+
+ +
+
+ +
+
+
+ ); +} diff --git a/src/components/auth/SignUpPage.tsx b/src/components/auth/SignUpPage.tsx new file mode 100644 index 0000000..bcce5e2 --- /dev/null +++ b/src/components/auth/SignUpPage.tsx @@ -0,0 +1,36 @@ +import { SignUp } from '@clerk/clerk-react'; + +/** + * Sign-up page with Clerk authentication + */ +export function SignUpPage() { + return ( +
+
+

+ Create your account +

+

+ Start reporting and tracking beach litter in your area +

+
+ +
+
+ +
+
+
+ ); +} diff --git a/src/components/auth/UserProfile.tsx b/src/components/auth/UserProfile.tsx new file mode 100644 index 0000000..4512ed5 --- /dev/null +++ b/src/components/auth/UserProfile.tsx @@ -0,0 +1,58 @@ +import { useUser, useClerk } from '@clerk/clerk-react'; +import { useNavigate } from 'react-router-dom'; + +/** + * User profile display component showing user info and sign-out option + */ +export function UserProfile() { + const { user, isLoaded } = useUser(); + const { signOut } = useClerk(); + const navigate = useNavigate(); + + const handleSignOut = async () => { + await signOut(); + navigate('/'); + }; + + if (!isLoaded) { + return ( +
+
+
+
+
+ ); + } + + if (!user) { + return null; + } + + return ( +
+
+ {user.imageUrl && ( + {user.fullName + )} +
+

+ {user.fullName || user.username || 'User'} +

+

+ {user.primaryEmailAddress?.emailAddress} +

+
+
+ +
+ ); +} diff --git a/src/components/auth/index.ts b/src/components/auth/index.ts new file mode 100644 index 0000000..f2caa70 --- /dev/null +++ b/src/components/auth/index.ts @@ -0,0 +1,4 @@ +export { ProtectedRoute } from './ProtectedRoute'; +export { UserProfile } from './UserProfile'; +export { SignInPage } from './SignInPage'; +export { SignUpPage } from './SignUpPage'; diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..ce08016 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,8 @@ +// Auth components +export * from './auth'; + +// Layout components +export * from './layout'; + +// Map components +export * from './map'; diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx new file mode 100644 index 0000000..12fa26d --- /dev/null +++ b/src/components/layout/Header.tsx @@ -0,0 +1,73 @@ +import { SignedIn, SignedOut, SignInButton, SignUpButton } from '@clerk/clerk-react'; +import { Link } from 'react-router-dom'; +import { UserProfile } from '../auth/UserProfile'; + +/** + * Application header with navigation and authentication controls + */ +export function Header() { + return ( +
+
+
+
+ +

+ Beach Litter Mapping +

+ +
+ + +
+
+
+ ); +} diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx new file mode 100644 index 0000000..808c288 --- /dev/null +++ b/src/components/layout/Layout.tsx @@ -0,0 +1,18 @@ +import { ReactNode } from 'react'; +import { Header } from './Header'; + +interface LayoutProps { + children: ReactNode; +} + +/** + * Main layout wrapper with header + */ +export function Layout({ children }: LayoutProps) { + return ( +
+
+
{children}
+
+ ); +} diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts new file mode 100644 index 0000000..beab924 --- /dev/null +++ b/src/components/layout/index.ts @@ -0,0 +1,2 @@ +export { Header } from './Header'; +export { Layout } from './Layout'; diff --git a/src/components/map/MapFilters.tsx b/src/components/map/MapFilters.tsx new file mode 100644 index 0000000..8cf72c7 --- /dev/null +++ b/src/components/map/MapFilters.tsx @@ -0,0 +1,217 @@ +import { useState } from 'react'; +import type { FilterState, LitterType, QuantityLevel } from '@/types'; + +interface MapFiltersProps { + filters: FilterState; + onFiltersChange: (filters: FilterState) => void; + onToggleClusters: () => void; + showClusters: boolean; +} + +export function MapFilters({ + filters, + onFiltersChange, + onToggleClusters, + showClusters, +}: MapFiltersProps) { + const [isExpanded, setIsExpanded] = useState(false); + + const litterTypes: LitterType[] = ['plastic', 'metal', 'glass', 'organic', 'hazardous', 'other']; + const quantities: QuantityLevel[] = ['minimal', 'moderate', 'significant', 'severe']; + + const handleLitterTypeToggle = (type: LitterType) => { + const currentTypes = filters.litterTypes || []; + const newTypes = currentTypes.includes(type) + ? currentTypes.filter(t => t !== type) + : [...currentTypes, type]; + + onFiltersChange({ + ...filters, + litterTypes: newTypes.length > 0 ? newTypes : undefined, + }); + }; + + const handleQuantityToggle = (quantity: QuantityLevel) => { + const currentQuantities = filters.quantities || []; + const newQuantities = currentQuantities.includes(quantity) + ? currentQuantities.filter(q => q !== quantity) + : [...currentQuantities, quantity]; + + onFiltersChange({ + ...filters, + quantities: newQuantities.length > 0 ? newQuantities : undefined, + }); + }; + + const handleDateRangeChange = (field: 'start' | 'end', value: string) => { + if (!value) { + if (field === 'start' && !filters.dateRange?.end) { + onFiltersChange({ ...filters, dateRange: undefined }); + } else if (field === 'end' && !filters.dateRange?.start) { + onFiltersChange({ ...filters, dateRange: undefined }); + } else { + onFiltersChange({ + ...filters, + dateRange: { + start: field === 'start' ? new Date() : filters.dateRange!.start, + end: field === 'end' ? new Date() : filters.dateRange!.end, + [field]: undefined, + } as any, + }); + } + return; + } + + const date = new Date(value); + onFiltersChange({ + ...filters, + dateRange: { + start: field === 'start' ? date : filters.dateRange?.start || date, + end: field === 'end' ? date : filters.dateRange?.end || date, + }, + }); + }; + + const handleClearFilters = () => { + onFiltersChange({}); + }; + + const hasActiveFilters = + (filters.litterTypes && filters.litterTypes.length > 0) || + (filters.quantities && filters.quantities.length > 0) || + filters.dateRange; + + return ( +
+ {/* Header */} +
+

Filters

+ +
+ + {/* Filter Content */} + {isExpanded && ( +
+ {/* Cluster Toggle */} +
+ + +
+ + {/* Date Range */} +
+ +
+ handleDateRangeChange('start', e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="Start date" + /> + handleDateRangeChange('end', e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="End date" + /> +
+
+ + {/* Litter Type */} +
+ +
+ {litterTypes.map((type) => ( + + ))} +
+
+ + {/* Quantity Level */} +
+ +
+ {quantities.map((quantity) => ( + + ))} +
+
+ + {/* Clear Filters Button */} + {hasActiveFilters && ( + + )} +
+ )} + + {/* Active Filters Badge */} + {!isExpanded && hasActiveFilters && ( +
+ {[ + filters.litterTypes?.length && `${filters.litterTypes.length} type(s)`, + filters.quantities?.length && `${filters.quantities.length} quantity`, + filters.dateRange && 'date range', + ].filter(Boolean).join(', ')} +
+ )} +
+ ); +} diff --git a/src/components/map/MapView.tsx b/src/components/map/MapView.tsx new file mode 100644 index 0000000..d7d8040 --- /dev/null +++ b/src/components/map/MapView.tsx @@ -0,0 +1,404 @@ +import { useEffect, useRef } from 'react'; +import L from 'leaflet'; +import 'leaflet/dist/leaflet.css'; +import 'leaflet.markercluster/dist/MarkerCluster.css'; +import 'leaflet.markercluster/dist/MarkerCluster.Default.css'; +import 'leaflet.markercluster'; +import type { Report, CleanupEvent, FilterState } from '@/types'; + +// Fix for default marker icons in Leaflet with Vite +import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png'; +import markerIcon from 'leaflet/dist/images/marker-icon.png'; +import markerShadow from 'leaflet/dist/images/marker-shadow.png'; + +delete (L.Icon.Default.prototype as any)._getIconUrl; +L.Icon.Default.mergeOptions({ + iconUrl: markerIcon, + iconRetinaUrl: markerIcon2x, + shadowUrl: markerShadow, +}); + +interface MapViewProps { + reports: Report[]; + events?: CleanupEvent[]; + filters?: FilterState; + onMarkerClick?: (reportId: string) => void; + onMapClick?: (lat: number, lng: number) => void; + showHeatmap?: boolean; + showClusters?: boolean; + center?: [number, number]; + zoom?: number; +} + +export function MapView({ + reports, + events = [], + filters, + onMarkerClick, + onMapClick, + showClusters = true, + center = [53.3498, -6.2603], // Dublin, Ireland default + zoom = 7, +}: MapViewProps) { + const mapRef = useRef(null); + const mapInstanceRef = useRef(null); + const markersLayerRef = useRef(null); + const clusterGroupRef = useRef(null); + + // Initialize map + useEffect(() => { + if (!mapRef.current || mapInstanceRef.current) return; + + // Create map instance + const map = L.map(mapRef.current).setView(center, zoom); + + // Add OpenStreetMap tiles + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', + maxZoom: 19, + }).addTo(map); + + // Handle map clicks + if (onMapClick) { + map.on('click', (e: L.LeafletMouseEvent) => { + onMapClick(e.latlng.lat, e.latlng.lng); + }); + } + + // Create markers layer + markersLayerRef.current = L.layerGroup(); + + // Create cluster group with custom configuration + clusterGroupRef.current = L.markerClusterGroup({ + maxClusterRadius: 50, // Cluster markers within 50 pixels + spiderfyOnMaxZoom: true, + showCoverageOnHover: true, + zoomToBoundsOnClick: true, + iconCreateFunction: (cluster) => { + const count = cluster.getChildCount(); + let sizeClass = 'marker-cluster-small'; + + if (count > 10) { + sizeClass = 'marker-cluster-large'; + } else if (count > 5) { + sizeClass = 'marker-cluster-medium'; + } + + return L.divIcon({ + html: `
${count}
`, + className: `marker-cluster ${sizeClass}`, + iconSize: L.point(40, 40), + }); + }, + }); + + mapInstanceRef.current = map; + + return () => { + map.remove(); + mapInstanceRef.current = null; + markersLayerRef.current = null; + clusterGroupRef.current = null; + }; + }, []); + + // Filter reports based on filter state + const getFilteredReports = (): Report[] => { + if (!filters) return reports; + + return reports.filter((report) => { + // Filter by date range + if (filters.dateRange) { + const reportDate = new Date(report.createdAt); + if (filters.dateRange.start && reportDate < filters.dateRange.start) { + return false; + } + if (filters.dateRange.end && reportDate > filters.dateRange.end) { + return false; + } + } + + // Filter by litter type + if (filters.litterTypes && filters.litterTypes.length > 0) { + if (!filters.litterTypes.includes(report.litterType)) { + return false; + } + } + + // Filter by quantity + if (filters.quantities && filters.quantities.length > 0) { + if (!filters.quantities.includes(report.quantity)) { + return false; + } + } + + // Filter by verification status + if (filters.verificationStatus && filters.verificationStatus.length > 0) { + if (!filters.verificationStatus.includes(report.verificationStatus)) { + return false; + } + } + + return true; + }); + }; + + // Update markers when reports, filters, or clustering changes + useEffect(() => { + if (!mapInstanceRef.current || !markersLayerRef.current || !clusterGroupRef.current) return; + + const map = mapInstanceRef.current; + const markersLayer = markersLayerRef.current; + const clusterGroup = clusterGroupRef.current; + + // Remove existing layers from map + if (map.hasLayer(markersLayer)) { + map.removeLayer(markersLayer); + } + if (map.hasLayer(clusterGroup)) { + map.removeLayer(clusterGroup); + } + + // Clear existing markers + markersLayer.clearLayers(); + clusterGroup.clearLayers(); + + // Get filtered reports + const filteredReports = getFilteredReports(); + + // Add report markers + filteredReports.forEach((report) => { + const icon = createCustomMarkerIcon(report.litterType, report.quantity); + const marker = L.marker([report.latitude, report.longitude], { icon }); + + // Add popup with report details + marker.bindPopup(` +
+

${getLitterTypeLabel(report.litterType)}

+

Quantity: ${report.quantity}

+

Status: ${report.verificationStatus}

+

Date: ${new Date(report.createdAt).toLocaleDateString()}

+ ${report.description ? `

${report.description}

` : ''} +
+ `); + + // Handle marker click + if (onMarkerClick) { + marker.on('click', () => { + onMarkerClick(report.id); + }); + } + + // Add to appropriate layer + if (showClusters) { + clusterGroup.addLayer(marker); + } else { + markersLayer.addLayer(marker); + } + }); + + // Add event markers (always to regular layer, not clustered) + events.forEach((event) => { + const icon = createEventMarkerIcon(event.status); + const marker = L.marker([event.latitude, event.longitude], { icon }); + + marker.bindPopup(` +
+

${event.title}

+

${event.locationName}

+

Participants: ${event.participantCount}

+

Status: ${event.status}

+
+ `); + + markersLayer.addLayer(marker); + }); + + // Add appropriate layer to map + if (showClusters) { + clusterGroup.addTo(map); + } + markersLayer.addTo(map); + }, [reports, events, filters, showClusters, onMarkerClick]); + + // Handle geolocation + const handleGeolocation = () => { + if (!navigator.geolocation) { + alert('Geolocation is not supported by your browser'); + return; + } + + navigator.geolocation.getCurrentPosition( + (position) => { + const coords: [number, number] = [ + position.coords.latitude, + position.coords.longitude, + ]; + + if (mapInstanceRef.current) { + mapInstanceRef.current.setView(coords, 13); + + // Add user location marker + L.marker(coords, { + icon: L.divIcon({ + className: 'user-location-marker', + html: '
', + iconSize: [16, 16], + }), + }).addTo(mapInstanceRef.current); + } + }, + (error) => { + alert(`Error getting location: ${error.message}`); + }, + { + enableHighAccuracy: true, + timeout: 10000, + maximumAge: 0, + } + ); + }; + + return ( +
+
+ + {/* Geolocation button */} + +
+ ); +} + +// Helper function to create custom marker icons based on litter type +function createCustomMarkerIcon(litterType: string, quantity: string): L.DivIcon { + const colors = { + plastic: '#3B82F6', // blue + metal: '#6B7280', // gray + glass: '#10B981', // green + organic: '#84CC16', // lime + hazardous: '#EF4444', // red + other: '#8B5CF6', // purple + }; + + const sizes = { + minimal: 24, + moderate: 28, + significant: 32, + severe: 36, + }; + + const color = colors[litterType as keyof typeof colors] || colors.other; + const size = sizes[quantity as keyof typeof sizes] || sizes.moderate; + + return L.divIcon({ + className: 'custom-marker', + html: ` +
+
+ ${getLitterTypeIcon(litterType)} +
+
+ `, + iconSize: [size, size], + iconAnchor: [size / 2, size], + }); +} + +// Helper function to create event marker icons +function createEventMarkerIcon(status: string): L.DivIcon { + const colors = { + upcoming: '#F59E0B', // amber + completed: '#10B981', // green + cancelled: '#6B7280', // gray + }; + + const color = colors[status as keyof typeof colors] || colors.upcoming; + + return L.divIcon({ + className: 'event-marker', + html: ` +
+ 📅 +
+ `, + iconSize: [32, 32], + iconAnchor: [16, 16], + }); +} + +// Helper function to get litter type icon +function getLitterTypeIcon(litterType: string): string { + const icons: Record = { + plastic: '♻', + metal: '⚙', + glass: '◆', + organic: '🌿', + hazardous: '⚠', + other: '●', + }; + const icon = icons[litterType]; + return icon !== undefined ? icon : '●'; +} + +// Helper function to get litter type label +function getLitterTypeLabel(litterType: string): string { + const label = litterType.charAt(0).toUpperCase() + litterType.slice(1); + return label; +} diff --git a/src/components/map/README.md b/src/components/map/README.md new file mode 100644 index 0000000..92f9ca7 --- /dev/null +++ b/src/components/map/README.md @@ -0,0 +1,80 @@ +# Map Components + +## MapView + +Interactive map component built with Leaflet.js for displaying litter reports and cleanup events. + +### Features + +- **OpenStreetMap Integration**: Uses free OpenStreetMap tiles +- **Custom Markers**: Color-coded markers for different litter types with size based on quantity +- **Geolocation**: Button to center map on user's current location +- **Interactive Popups**: Click markers to view report details +- **Event Markers**: Display cleanup events with distinct styling +- **Zoom & Pan Controls**: Standard Leaflet map controls + +### Usage + +```tsx +import { MapView } from '@/components/map'; + +function MyMapPage() { + const reports = [/* array of Report objects */]; + const events = [/* array of CleanupEvent objects */]; + + const handleMarkerClick = (reportId: string) => { + console.log('Report clicked:', reportId); + }; + + return ( + + ); +} +``` + +### Props + +- `reports` (Report[]): Array of litter reports to display +- `events` (CleanupEvent[]): Array of cleanup events to display (optional) +- `filters` (FilterState): Filter state for reports (optional, for future use) +- `onMarkerClick` ((reportId: string) => void): Callback when a report marker is clicked (optional) +- `onMapClick` ((lat: number, lng: number) => void): Callback when the map is clicked (optional) +- `showHeatmap` (boolean): Toggle heat map overlay (optional, for future use) +- `showClusters` (boolean): Toggle marker clustering (optional, for future use) +- `center` ([number, number]): Initial map center coordinates (default: Dublin, Ireland) +- `zoom` (number): Initial zoom level (default: 7) + +### Marker Colors + +- **Plastic**: Blue (#3B82F6) +- **Metal**: Gray (#6B7280) +- **Glass**: Green (#10B981) +- **Organic**: Lime (#84CC16) +- **Hazardous**: Red (#EF4444) +- **Other**: Purple (#8B5CF6) + +### Marker Sizes (by quantity) + +- **Minimal**: 24px +- **Moderate**: 28px +- **Significant**: 32px +- **Severe**: 36px + +### Event Marker Colors + +- **Upcoming**: Amber (#F59E0B) +- **Completed**: Green (#10B981) +- **Cancelled**: Gray (#6B7280) + +### Requirements Satisfied + +- ✅ 2.1: Display litter reports as visual markers on interactive map +- ✅ 2.2: Click markers to display full report details +- ✅ 2.3: Map zoom functionality from country to street level +- ✅ 8.2: Access device GPS to center map on user location diff --git a/src/components/map/index.ts b/src/components/map/index.ts new file mode 100644 index 0000000..084f264 --- /dev/null +++ b/src/components/map/index.ts @@ -0,0 +1,2 @@ +export { MapView } from './MapView'; +export { MapFilters } from './MapFilters'; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..d4097de --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,4 @@ +// Custom React hooks for the Beach Litter Mapping Platform + +export { default as useGeolocation } from './useGeolocation'; +export { useAuth } from './useAuth'; diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts new file mode 100644 index 0000000..c3fcf32 --- /dev/null +++ b/src/hooks/useAuth.ts @@ -0,0 +1,20 @@ +import { useUser, useAuth as useClerkAuth } from '@clerk/clerk-react'; + +/** + * Custom hook for authentication state and user data + * Wraps Clerk's useAuth and useUser hooks for easier access + */ +export function useAuth() { + const { isLoaded: authLoaded, isSignedIn, userId } = useClerkAuth(); + const { user, isLoaded: userLoaded } = useUser(); + + const isLoaded = authLoaded && userLoaded; + + return { + isLoaded, + isSignedIn, + userId, + user, + isAuthenticated: isSignedIn && !!user, + }; +} diff --git a/src/hooks/useGeolocation.ts b/src/hooks/useGeolocation.ts new file mode 100644 index 0000000..00d6842 --- /dev/null +++ b/src/hooks/useGeolocation.ts @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import type { Coordinates } from '@/types'; + +interface GeolocationState { + coordinates: Coordinates | null; + error: string | null; + loading: boolean; +} + +/** + * Custom hook to get user's current geolocation + */ +export default function useGeolocation() { + const [state, setState] = useState({ + coordinates: null, + error: null, + loading: false, + }); + + const getCurrentPosition = () => { + if (!navigator.geolocation) { + setState({ + coordinates: null, + error: 'Geolocation is not supported by your browser', + loading: false, + }); + return; + } + + setState(prev => ({ ...prev, loading: true, error: null })); + + navigator.geolocation.getCurrentPosition( + (position) => { + setState({ + coordinates: { + latitude: position.coords.latitude, + longitude: position.coords.longitude, + }, + error: null, + loading: false, + }); + }, + (error) => { + setState({ + coordinates: null, + error: error.message, + loading: false, + }); + }, + { + enableHighAccuracy: true, + timeout: 10000, + maximumAge: 0, + } + ); + }; + + return { + ...state, + getCurrentPosition, + }; +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..e66692b --- /dev/null +++ b/src/index.css @@ -0,0 +1,80 @@ +@import "tailwindcss"; + +/* Leaflet CSS overrides */ +.leaflet-container { + font-family: inherit; +} + +/* Ensure map markers are clickable */ +.leaflet-marker-icon { + cursor: pointer; +} + +/* Custom marker cluster styles */ +.marker-cluster { + background-clip: padding-box; + border-radius: 50%; +} + +.marker-cluster div { + width: 30px; + height: 30px; + margin-left: 5px; + margin-top: 5px; + text-align: center; + border-radius: 50%; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; +} + +.marker-cluster-small { + background-color: rgba(59, 130, 246, 0.6); +} + +.marker-cluster-small div { + background-color: rgba(59, 130, 246, 0.8); + color: white; + font-size: 12px; +} + +.marker-cluster-medium { + background-color: rgba(249, 115, 22, 0.6); +} + +.marker-cluster-medium div { + background-color: rgba(249, 115, 22, 0.8); + color: white; + font-size: 13px; +} + +.marker-cluster-large { + background-color: rgba(239, 68, 68, 0.6); +} + +.marker-cluster-large div { + background-color: rgba(239, 68, 68, 0.8); + color: white; + font-size: 14px; +} + +/* User location marker animation */ +.user-location-marker { + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.2); + opacity: 0.7; + } + 100% { + transform: scale(1); + opacity: 1; + } +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..744fdc8 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,16 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import './index.css'; +import App from './App'; + +const rootElement = document.getElementById('root'); + +if (!rootElement) { + throw new Error('Root element not found'); +} + +createRoot(rootElement).render( + + + +); diff --git a/src/pages/AnalyticsPage.tsx b/src/pages/AnalyticsPage.tsx new file mode 100644 index 0000000..8219f3f --- /dev/null +++ b/src/pages/AnalyticsPage.tsx @@ -0,0 +1,15 @@ +/** + * Analytics page - placeholder for task 10-12 + */ +export function AnalyticsPage() { + return ( +
+

Analytics Dashboard

+
+

+ Analytics dashboard will be implemented in tasks 10-12 +

+
+
+ ); +} diff --git a/src/pages/EventsPage.tsx b/src/pages/EventsPage.tsx new file mode 100644 index 0000000..fc7d627 --- /dev/null +++ b/src/pages/EventsPage.tsx @@ -0,0 +1,15 @@ +/** + * Events page - placeholder for task 13-14 + */ +export function EventsPage() { + return ( +
+

Clean-up Events

+
+

+ Events management will be implemented in tasks 13-14 +

+
+
+ ); +} diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx new file mode 100644 index 0000000..68d27f6 --- /dev/null +++ b/src/pages/HomePage.tsx @@ -0,0 +1,90 @@ +import { SignedIn, SignedOut } from '@clerk/clerk-react'; +import { Link } from 'react-router-dom'; + +/** + * Home page with welcome message and call-to-action + */ +export function HomePage() { + return ( +
+
+

+ Track and Clean Ireland's Beaches +

+

+ Join our community in mapping beach litter, organizing clean-up events, + and protecting our coastal environment through data-driven action. +

+ + +
+ + Get Started + + + View Map + +
+
+ + +
+ + Report Litter + + + View Map + +
+
+
+ +
+
+
📍
+

+ Report Litter +

+

+ Document beach litter with photos and location data to raise awareness + and track pollution patterns. +

+
+ +
+
🗺️
+

+ Interactive Maps +

+

+ Visualize litter hotspots, view reports from your area, and identify + areas that need attention. +

+
+ +
+
🤝
+

+ Organize Events +

+

+ Create and join clean-up events, coordinate with volunteers, and make + a real impact on coastal health. +

+
+
+
+ ); +} diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx new file mode 100644 index 0000000..d38a32b --- /dev/null +++ b/src/pages/MapPage.tsx @@ -0,0 +1,165 @@ +import { useState } from 'react'; +import { MapView, MapFilters } from '@/components/map'; +import type { Report, FilterState } from '@/types'; + +/** + * Map page - Interactive map displaying litter reports and cleanup events + */ +export function MapPage() { + const [selectedReportId, setSelectedReportId] = useState(null); + const [filters, setFilters] = useState({}); + const [showClusters, setShowClusters] = useState(true); + + // Sample data for demonstration (will be replaced with API data in later tasks) + const sampleReports: Report[] = [ + { + id: '1', + userId: 'user1', + latitude: 53.3498, + longitude: -6.2603, + locationSource: 'gps', + photoUrls: ['https://example.com/photo1.jpg'], + litterType: 'plastic', + quantity: 'moderate', + description: 'Plastic bottles and bags near the shore', + createdAt: new Date('2024-01-15'), + updatedAt: new Date('2024-01-15'), + submittedAt: new Date('2024-01-15'), + verificationStatus: 'verified', + }, + { + id: '2', + userId: 'user2', + latitude: 53.2707, + longitude: -9.0568, + locationSource: 'gps', + photoUrls: ['https://example.com/photo2.jpg'], + litterType: 'glass', + quantity: 'minimal', + description: 'Broken glass bottles', + createdAt: new Date('2024-01-16'), + updatedAt: new Date('2024-01-16'), + submittedAt: new Date('2024-01-16'), + verificationStatus: 'pending', + }, + { + id: '3', + userId: 'user3', + latitude: 51.8969, + longitude: -8.4863, + locationSource: 'manual', + photoUrls: ['https://example.com/photo3.jpg'], + litterType: 'metal', + quantity: 'significant', + description: 'Metal cans and containers', + createdAt: new Date('2024-01-17'), + updatedAt: new Date('2024-01-17'), + submittedAt: new Date('2024-01-17'), + verificationStatus: 'verified', + }, + { + id: '4', + userId: 'user4', + latitude: 53.3520, + longitude: -6.2650, + locationSource: 'gps', + photoUrls: ['https://example.com/photo4.jpg'], + litterType: 'plastic', + quantity: 'severe', + description: 'Large amount of plastic waste', + createdAt: new Date('2024-01-18'), + updatedAt: new Date('2024-01-18'), + submittedAt: new Date('2024-01-18'), + verificationStatus: 'verified', + }, + { + id: '5', + userId: 'user5', + latitude: 53.3450, + longitude: -6.2580, + locationSource: 'gps', + photoUrls: ['https://example.com/photo5.jpg'], + litterType: 'hazardous', + quantity: 'moderate', + description: 'Chemical containers', + createdAt: new Date('2024-01-19'), + updatedAt: new Date('2024-01-19'), + submittedAt: new Date('2024-01-19'), + verificationStatus: 'disputed', + }, + { + id: '6', + userId: 'user6', + latitude: 53.3480, + longitude: -6.2620, + locationSource: 'manual', + photoUrls: ['https://example.com/photo6.jpg'], + litterType: 'organic', + quantity: 'minimal', + description: 'Seaweed and organic debris', + createdAt: new Date('2024-01-20'), + updatedAt: new Date('2024-01-20'), + submittedAt: new Date('2024-01-20'), + verificationStatus: 'verified', + }, + ]; + + const handleMarkerClick = (reportId: string) => { + setSelectedReportId(reportId); + console.log('Report clicked:', reportId); + // In later tasks, this will open a modal with full report details + }; + + const handleMapClick = (lat: number, lng: number) => { + console.log('Map clicked at:', lat, lng); + // In later tasks, this can be used for manual location selection + }; + + const handleToggleClusters = () => { + setShowClusters(!showClusters); + }; + + return ( +
+
+

Interactive Map

+

+ View litter reports and cleanup events across Ireland +

+
+ +
+
+ + + +
+
+ + {selectedReportId && ( +
+

+ Selected Report ID: {selectedReportId} +

+ +
+ )} +
+ ); +} diff --git a/src/pages/README.md b/src/pages/README.md new file mode 100644 index 0000000..b15fb1a --- /dev/null +++ b/src/pages/README.md @@ -0,0 +1,13 @@ +# Pages Directory + +This directory contains page-level components that represent different routes in the application. + +## Planned Pages + +- `HomePage.tsx` - Landing page with map view +- `ReportPage.tsx` - Report submission page +- `AnalyticsPage.tsx` - Analytics dashboard +- `EventsPage.tsx` - Event listing and management +- `EventDetailPage.tsx` - Individual event details +- `ProfilePage.tsx` - User profile page +- `NotFoundPage.tsx` - 404 error page diff --git a/src/pages/ReportFormPage.tsx b/src/pages/ReportFormPage.tsx new file mode 100644 index 0000000..ef249ac --- /dev/null +++ b/src/pages/ReportFormPage.tsx @@ -0,0 +1,15 @@ +/** + * Report form page - placeholder for task 6-9 + */ +export function ReportFormPage() { + return ( +
+

Report Litter

+
+

+ Report form will be implemented in tasks 6-9 +

+
+
+ ); +} diff --git a/src/pages/index.ts b/src/pages/index.ts new file mode 100644 index 0000000..26ba3f7 --- /dev/null +++ b/src/pages/index.ts @@ -0,0 +1,5 @@ +export { HomePage } from './HomePage'; +export { MapPage } from './MapPage'; +export { ReportFormPage } from './ReportFormPage'; +export { EventsPage } from './EventsPage'; +export { AnalyticsPage } from './AnalyticsPage'; diff --git a/src/services/README.md b/src/services/README.md new file mode 100644 index 0000000..d6141f1 --- /dev/null +++ b/src/services/README.md @@ -0,0 +1,13 @@ +# Services Directory + +This directory contains service modules for API communication and external integrations. + +## Planned Services + +- `api.ts` - Base API client (already created) +- `reportService.ts` - Report-related API calls +- `eventService.ts` - Event-related API calls +- `analyticsService.ts` - Analytics API calls +- `userService.ts` - User profile API calls +- `exifService.ts` - EXIF metadata extraction +- `externalApiService.ts` - Weather, tides, water quality APIs diff --git a/src/services/api.ts b/src/services/api.ts new file mode 100644 index 0000000..d4565f7 --- /dev/null +++ b/src/services/api.ts @@ -0,0 +1,66 @@ +// API service configuration and base client + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api'; + +export interface ApiError { + code: string; + message: string; + details?: any; + timestamp: string; +} + +export class ApiClient { + private baseUrl: string; + + constructor(baseUrl: string = API_BASE_URL) { + this.baseUrl = baseUrl; + } + + private async request( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = `${this.baseUrl}${endpoint}`; + + const response = await fetch(url, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }); + + if (!response.ok) { + const error: ApiError = await response.json(); + throw new Error(error.message || 'An error occurred'); + } + + return response.json(); + } + + async get(endpoint: string, options?: RequestInit): Promise { + return this.request(endpoint, { ...options, method: 'GET' }); + } + + async post(endpoint: string, data?: any, options?: RequestInit): Promise { + return this.request(endpoint, { + ...options, + method: 'POST', + body: JSON.stringify(data), + }); + } + + async patch(endpoint: string, data?: any, options?: RequestInit): Promise { + return this.request(endpoint, { + ...options, + method: 'PATCH', + body: JSON.stringify(data), + }); + } + + async delete(endpoint: string, options?: RequestInit): Promise { + return this.request(endpoint, { ...options, method: 'DELETE' }); + } +} + +export const apiClient = new ApiClient(); diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..cc46db0 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,121 @@ +// Core type definitions for the Beach Litter Mapping Platform + +export type LitterType = 'plastic' | 'metal' | 'glass' | 'organic' | 'hazardous' | 'other'; + +export type QuantityLevel = 'minimal' | 'moderate' | 'significant' | 'severe'; + +export type LocationSource = 'exif' | 'gps' | 'manual'; + +export type VerificationStatus = 'pending' | 'verified' | 'disputed'; + +export type EventStatus = 'upcoming' | 'completed' | 'cancelled'; + +export interface Coordinates { + latitude: number; + longitude: number; +} + +export interface User { + id: string; + clerkId: string; + email: string; + username: string; + firstName?: string; + lastName?: string; + avatarUrl?: string; + impactScore: number; + createdAt: Date; + updatedAt: Date; +} + +export interface Report { + id: string; + userId: string; + latitude: number; + longitude: number; + locationSource: LocationSource; + photoUrls: string[]; + photoTimestamp?: Date; + litterType: LitterType; + quantity: QuantityLevel; + description?: string; + createdAt: Date; + updatedAt: Date; + submittedAt: Date; + verificationStatus: VerificationStatus; +} + +export interface CleanupEvent { + id: string; + organizerId: string; + title: string; + description: string; + latitude: number; + longitude: number; + locationName: string; + scheduledDate: Date; + duration: number; // in minutes + status: EventStatus; + participantCount: number; + litterCollected?: number; // in kilograms + photos?: string[]; + createdAt: Date; + updatedAt: Date; +} + +export interface EnvironmentalConcern { + id: string; + reportId: string; + concernType: string; + severity: 'low' | 'medium' | 'high' | 'critical'; + description: string; + createdAt: Date; +} + +export interface Hotspot { + id: string; + latitude: number; + longitude: number; + radius: number; // in meters + reportCount: number; + severityScore: number; + lastReportDate: Date; + calculatedAt: Date; +} + +// DTOs (Data Transfer Objects) +export interface CreateReportDTO { + photos: File[]; + latitude?: number; + longitude?: number; + locationSource: LocationSource; + litterType: LitterType; + quantity: QuantityLevel; + description?: string; + environmentalConcerns?: { + type: string; + severity: 'low' | 'medium' | 'high' | 'critical'; + description: string; + }[]; +} + +export interface ReportResponseDTO extends Report { + user: { + id: string; + username: string; + avatarUrl?: string; + }; + verificationCount: number; + disputeCount: number; + environmentalConcerns: EnvironmentalConcern[]; +} + +export interface FilterState { + dateRange?: { + start: Date; + end: Date; + }; + litterTypes?: LitterType[]; + quantities?: QuantityLevel[]; + verificationStatus?: VerificationStatus[]; +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..053d399 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,71 @@ +// Utility functions for the Beach Litter Mapping Platform + +/** + * Validates if coordinates are within Ireland's geographical bounds + */ +export function isValidIrishCoordinates(lat: number, lng: number): boolean { + // Ireland approximate bounds + const MIN_LAT = 51.4; + const MAX_LAT = 55.4; + const MIN_LNG = -10.5; + const MAX_LNG = -5.5; + + return lat >= MIN_LAT && lat <= MAX_LAT && lng >= MIN_LNG && lng <= MAX_LNG; +} + +/** + * Formats a date to ISO 8601 format + */ +export function formatDateISO(date: Date): string { + return date.toISOString(); +} + +/** + * Calculates distance between two coordinates in meters using Haversine formula + */ +export function calculateDistance( + lat1: number, + lng1: number, + lat2: number, + lng2: number +): number { + const R = 6371e3; // Earth's radius in meters + const φ1 = (lat1 * Math.PI) / 180; + const φ2 = (lat2 * Math.PI) / 180; + const Δφ = ((lat2 - lat1) * Math.PI) / 180; + const Δλ = ((lng2 - lng1) * Math.PI) / 180; + + const a = + Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return R * c; +} + +/** + * Validates file type for photo uploads + */ +export function isValidImageFile(file: File): boolean { + const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; + return validTypes.includes(file.type); +} + +/** + * Validates file size (max 5MB) + */ +export function isValidFileSize(file: File, maxSizeMB: number = 5): boolean { + const maxSizeBytes = maxSizeMB * 1024 * 1024; + return file.size <= maxSizeBytes; +} + +/** + * Formats bytes to human-readable size + */ +export function formatFileSize(bytes: number): string { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..72dac7b --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + readonly VITE_CLERK_PUBLISHABLE_KEY: string; + readonly VITE_API_BASE_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..dca8ba0 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..da81b2b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + + /* Path aliases */ + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@/components/*": ["./src/components/*"], + "@/pages/*": ["./src/pages/*"], + "@/services/*": ["./src/services/*"], + "@/hooks/*": ["./src/hooks/*"], + "@/utils/*": ["./src/utils/*"], + "@/types/*": ["./src/types/*"] + } + }, + "include": ["src"] +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..4139dd8 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + '@/components': path.resolve(__dirname, './src/components'), + '@/pages': path.resolve(__dirname, './src/pages'), + '@/services': path.resolve(__dirname, './src/services'), + '@/hooks': path.resolve(__dirname, './src/hooks'), + '@/utils': path.resolve(__dirname, './src/utils'), + '@/types': path.resolve(__dirname, './src/types'), + }, + }, +})