From 651ce46183bdc6c9041184bd2aae403df69dde39 Mon Sep 17 00:00:00 2001 From: sstent Date: Thu, 11 Sep 2025 07:45:25 -0700 Subject: [PATCH] sync --- CL_backendfixes.md | 261 +++++ CL_frontendfixes.md | 255 +++++ backend/app/routes/export.py | 40 + backend/app/routes/import.py | 38 + backend/app/services/export_service.py | 138 +++ backend/app/services/import_service.py | 259 +++++ docker-compose.yml | 7 +- export_import_api_spec.md | 91 ++ export_import_frontend_spec.md | 221 ++++ frontend/Dockerfile | 6 +- frontend/package-lock.json | 567 ++++++++-- frontend/package.json | 7 + frontend/src/components/Navigation.jsx | 6 + .../src/components/plans/EditWorkoutModal.jsx | 135 +++ .../src/components/plans/GoalSelector.jsx | 97 ++ .../src/components/plans/PlanParameters.jsx | 129 +++ .../src/components/plans/PlanTimeline.jsx | 151 +-- frontend/src/components/plans/WorkoutCard.jsx | 45 + .../plans/__tests__/GoalSelector.test.jsx | 34 + frontend/src/components/routes/FileUpload.jsx | 231 +++++ .../src/components/routes/RouteFilter.jsx | 80 ++ frontend/src/components/routes/RouteList.jsx | 107 ++ .../src/components/routes/RouteMetadata.jsx | 61 ++ .../components/routes/RouteVisualization.jsx | 93 ++ .../src/components/routes/SectionList.jsx | 116 +++ .../src/components/routes/SectionManager.jsx | 104 ++ .../routes/__tests__/FileUpload.test.jsx | 26 + .../routes/__tests__/RouteList.test.jsx | 41 + .../routes/__tests__/RouteMetadata.test.jsx | 52 + .../__tests__/RouteVisualization.test.jsx | 26 + frontend/src/components/rules/RuleEditor.jsx | 121 +++ frontend/src/components/rules/RulePreview.jsx | 46 + frontend/src/components/rules/RulesList.jsx | 113 ++ .../rules/__tests__/RuleEditor.test.jsx | 45 + .../rules/__tests__/RulePreview.test.jsx | 20 + .../rules/__tests__/RulesList.test.jsx | 32 + .../src/components/ui/ProgressTracker.jsx | 38 + frontend/src/pages/PlanGeneration.jsx | 85 ++ frontend/src/pages/RoutesPage.jsx | 75 +- frontend/src/pages/Rules.jsx | 85 ++ frontend/src/pages/index.tsx | 4 +- frontend/src/services/planService.js | 47 + frontend/src/services/routeService.js | 64 ++ frontend/src/services/ruleService.js | 55 + package-lock.json | 970 +++++++++++++++++- package.json | 3 + 46 files changed, 5063 insertions(+), 164 deletions(-) create mode 100644 CL_backendfixes.md create mode 100644 CL_frontendfixes.md create mode 100644 backend/app/routes/export.py create mode 100644 backend/app/routes/import.py create mode 100644 backend/app/services/export_service.py create mode 100644 backend/app/services/import_service.py create mode 100644 export_import_api_spec.md create mode 100644 export_import_frontend_spec.md create mode 100644 frontend/src/components/plans/EditWorkoutModal.jsx create mode 100644 frontend/src/components/plans/GoalSelector.jsx create mode 100644 frontend/src/components/plans/PlanParameters.jsx create mode 100644 frontend/src/components/plans/WorkoutCard.jsx create mode 100644 frontend/src/components/plans/__tests__/GoalSelector.test.jsx create mode 100644 frontend/src/components/routes/FileUpload.jsx create mode 100644 frontend/src/components/routes/RouteFilter.jsx create mode 100644 frontend/src/components/routes/RouteList.jsx create mode 100644 frontend/src/components/routes/RouteMetadata.jsx create mode 100644 frontend/src/components/routes/RouteVisualization.jsx create mode 100644 frontend/src/components/routes/SectionList.jsx create mode 100644 frontend/src/components/routes/SectionManager.jsx create mode 100644 frontend/src/components/routes/__tests__/FileUpload.test.jsx create mode 100644 frontend/src/components/routes/__tests__/RouteList.test.jsx create mode 100644 frontend/src/components/routes/__tests__/RouteMetadata.test.jsx create mode 100644 frontend/src/components/routes/__tests__/RouteVisualization.test.jsx create mode 100644 frontend/src/components/rules/RuleEditor.jsx create mode 100644 frontend/src/components/rules/RulePreview.jsx create mode 100644 frontend/src/components/rules/RulesList.jsx create mode 100644 frontend/src/components/rules/__tests__/RuleEditor.test.jsx create mode 100644 frontend/src/components/rules/__tests__/RulePreview.test.jsx create mode 100644 frontend/src/components/rules/__tests__/RulesList.test.jsx create mode 100644 frontend/src/components/ui/ProgressTracker.jsx create mode 100644 frontend/src/pages/PlanGeneration.jsx create mode 100644 frontend/src/pages/Rules.jsx create mode 100644 frontend/src/services/planService.js create mode 100644 frontend/src/services/routeService.js create mode 100644 frontend/src/services/ruleService.js diff --git a/CL_backendfixes.md b/CL_backendfixes.md new file mode 100644 index 0000000..b098283 --- /dev/null +++ b/CL_backendfixes.md @@ -0,0 +1,261 @@ +# ๐ŸŽฏ **Backend Implementation TODO List** + +## **Priority 1: Core API Gaps (Essential)** + +### **1.1 Plan Generation Endpoint** +- [ ] **Add plan generation endpoint** in `app/routes/plan.py` + ```python + @router.post("/generate", response_model=PlanSchema) + async def generate_plan( + plan_request: PlanGenerationRequest, + db: AsyncSession = Depends(get_db) + ): + ``` +- [ ] **Create PlanGenerationRequest schema** in `app/schemas/plan.py` + ```python + class PlanGenerationRequest(BaseModel): + rule_ids: List[UUID] + goals: Dict[str, Any] + user_preferences: Optional[Dict[str, Any]] = None + duration_weeks: int = 12 + ``` +- [ ] **Update AIService.generate_plan()** to handle rule fetching from DB +- [ ] **Add validation** for rule compatibility and goal requirements +- [ ] **Add tests** for plan generation workflow + +### **1.2 Rule Parsing API** +- [ ] **Add natural language rule parsing endpoint** in `app/routes/rule.py` + ```python + @router.post("/parse-natural-language") + async def parse_natural_language_rules( + request: NaturalLanguageRuleRequest, + db: AsyncSession = Depends(get_db) + ): + ``` +- [ ] **Create request/response schemas** in `app/schemas/rule.py` + ```python + class NaturalLanguageRuleRequest(BaseModel): + natural_language_text: str + rule_name: str + + class ParsedRuleResponse(BaseModel): + parsed_rules: Dict[str, Any] + confidence_score: Optional[float] + suggestions: Optional[List[str]] + ``` +- [ ] **Enhance AIService.parse_rules_from_natural_language()** with better error handling +- [ ] **Add rule validation** after parsing +- [ ] **Add preview mode** before saving parsed rules + +### **1.3 Section Integration with GPX Parsing** +- [ ] **Update `app/services/gpx.py`** to create sections automatically + ```python + async def parse_gpx_with_sections(file_path: str, route_id: UUID, db: AsyncSession) -> dict: + # Parse GPX into segments + # Create Section records for each segment + # Return enhanced GPX data with section metadata + ``` +- [ ] **Modify `app/routes/gpx.py`** to create sections after route creation +- [ ] **Add section creation logic** in GPX upload workflow +- [ ] **Update Section model** to include more GPX-derived metadata +- [ ] **Add section querying endpoints** for route visualization + +## **Priority 2: Data Model Enhancements** + +### **2.1 Missing Schema Fields** +- [ ] **Add missing fields to User model** in `app/models/user.py` + ```python + class User(BaseModel): + name: Optional[str] + email: Optional[str] + fitness_level: Optional[str] + preferences: Optional[JSON] + ``` +- [ ] **Enhance Plan model** with additional metadata + ```python + class Plan(BaseModel): + user_id: Optional[UUID] = Column(ForeignKey("users.id")) + name: str + description: Optional[str] + start_date: Optional[Date] + end_date: Optional[Date] + goal_type: Optional[str] + active: Boolean = Column(default=True) + ``` +- [ ] **Add plan-rule relationship table** (already exists but ensure proper usage) +- [ ] **Update all schemas** to match enhanced models + +### **2.2 Database Relationships** +- [ ] **Fix User-Plan relationship** in models +- [ ] **Add cascade delete rules** where appropriate +- [ ] **Add database constraints** for data integrity +- [ ] **Create missing indexes** for performance + ```sql + CREATE INDEX idx_workouts_garmin_activity_id ON workouts(garmin_activity_id); + CREATE INDEX idx_plans_user_active ON plans(user_id, active); + CREATE INDEX idx_analyses_workout_approved ON analyses(workout_id, approved); + ``` + +## **Priority 3: API Completeness** + +### **3.1 Export/Import Functionality** +- [ ] **Create export service** `app/services/export_import.py` + ```python + class ExportImportService: + async def export_user_data(user_id: UUID) -> bytes: + async def export_routes() -> bytes: + async def import_user_data(data: bytes, user_id: UUID): + ``` +- [ ] **Add export endpoints** in new `app/routes/export.py` + ```python + @router.get("/export/routes") + @router.get("/export/plans/{plan_id}") + @router.get("/export/user-data") + @router.post("/import/routes") + @router.post("/import/plans") + ``` +- [ ] **Support multiple formats** (JSON, GPX, ZIP) +- [ ] **Add data validation** for imports +- [ ] **Handle version compatibility** for imports + +### **3.2 Enhanced Dashboard API** +- [ ] **Expand dashboard data** in `app/routes/dashboard.py` + ```python + @router.get("/metrics/weekly") + @router.get("/metrics/monthly") + @router.get("/progress/{plan_id}") + @router.get("/upcoming-workouts") + ``` +- [ ] **Add aggregation queries** for metrics +- [ ] **Cache dashboard data** for performance +- [ ] **Add real-time updates** capability + +### **3.3 Advanced Workout Features** +- [ ] **Add workout comparison endpoint** + ```python + @router.get("/workouts/{workout_id}/compare/{compare_workout_id}") + ``` +- [ ] **Add workout search/filtering** + ```python + @router.get("/workouts/search") + async def search_workouts( + activity_type: Optional[str] = None, + date_range: Optional[DateRange] = None, + power_range: Optional[PowerRange] = None + ): + ``` +- [ ] **Add bulk workout operations** +- [ ] **Add workout tagging system** + +## **Priority 4: Service Layer Improvements** + +### **4.1 AI Service Enhancements** +- [ ] **Add prompt caching** to reduce API calls +- [ ] **Implement prompt A/B testing** framework +- [ ] **Add AI response validation** and confidence scoring +- [ ] **Create AI service health checks** +- [ ] **Add fallback mechanisms** for AI failures +- [ ] **Implement rate limiting** for AI calls +- [ ] **Add cost tracking** for AI API usage + +### **4.2 Garmin Service Improvements** +- [ ] **Add incremental sync** instead of full sync +- [ ] **Implement activity deduplication** logic +- [ ] **Add webhook support** for real-time sync +- [ ] **Enhance error recovery** for failed syncs +- [ ] **Add activity type filtering** +- [ ] **Support multiple Garmin accounts** per user + +### **4.3 Plan Evolution Enhancements** +- [ ] **Add plan comparison** functionality +- [ ] **Implement plan rollback** mechanism +- [ ] **Add plan branching** for different scenarios +- [ ] **Create plan templates** system +- [ ] **Add automated plan adjustments** based on performance + +## **Priority 5: Validation & Error Handling** + +### **5.1 Input Validation** +- [ ] **Add comprehensive Pydantic validators** for all schemas +- [ ] **Validate GPX file integrity** before processing +- [ ] **Add business rule validation** (e.g., plan dates, workout conflicts) +- [ ] **Validate AI responses** before storing +- [ ] **Add file size/type restrictions** + +### **5.2 Error Handling** +- [ ] **Create custom exception hierarchy** + ```python + class CyclingCoachException(Exception): + class GarminSyncError(CyclingCoachException): + class AIServiceError(CyclingCoachException): + class PlanGenerationError(CyclingCoachException): + ``` +- [ ] **Add global exception handler** +- [ ] **Improve error messages** for user feedback +- [ ] **Add error recovery mechanisms** +- [ ] **Log errors with context** for debugging + +## **Priority 6: Performance & Monitoring** + +### **6.1 Performance Optimizations** +- [ ] **Add database query optimization** +- [ ] **Implement caching** for frequently accessed data +- [ ] **Add connection pooling** configuration +- [ ] **Optimize GPX file parsing** for large files +- [ ] **Add pagination** to list endpoints +- [ ] **Implement background job queue** for long-running tasks + +### **6.2 Enhanced Monitoring** +- [ ] **Add application metrics** (response times, error rates) +- [ ] **Create health check dependencies** +- [ ] **Add performance profiling** endpoints +- [ ] **Implement alerting** for critical errors +- [ ] **Add audit logging** for data changes + +## **Priority 7: Security & Configuration** + +### **7.1 Security Improvements** +- [ ] **Implement user authentication/authorization** +- [ ] **Add rate limiting** to prevent abuse +- [ ] **Validate file uploads** for security +- [ ] **Add CORS configuration** properly +- [ ] **Implement request/response logging** (without sensitive data) +- [ ] **Add API versioning** support + +### **7.2 Configuration Management** +- [ ] **Add environment-specific configs** +- [ ] **Validate configuration** on startup +- [ ] **Add feature flags** system +- [ ] **Implement secrets management** +- [ ] **Add configuration reload** without restart + +## **Priority 8: Testing & Documentation** + +### **8.1 Testing** +- [ ] **Create comprehensive test suite** + - Unit tests for services + - Integration tests for API endpoints + - Database migration tests + - AI service mock tests +- [ ] **Add test fixtures** for common data +- [ ] **Implement test database** setup/teardown +- [ ] **Add performance tests** for critical paths +- [ ] **Create end-to-end tests** for workflows + +### **8.2 Documentation** +- [ ] **Generate OpenAPI documentation** +- [ ] **Add endpoint documentation** with examples +- [ ] **Create service documentation** +- [ ] **Document deployment procedures** +- [ ] **Add troubleshooting guides** + +--- + +## **๐ŸŽฏ Recommended Implementation Order:** + +1. **Week 1:** Priority 1 (Core API gaps) - Essential for feature completeness +2. **Week 2:** Priority 2 (Data model) + Priority 5.1 (Validation) - Foundation improvements +3. **Week 3:** Priority 3.1 (Export/Import) + Priority 4.1 (AI improvements) - User-facing features +4. **Week 4:** Priority 6 (Performance) + Priority 8.1 (Testing) - Production readiness + +This todo list will bring your backend implementation to 100% design doc compliance and beyond, making it production-ready with enterprise-level features! ๐Ÿš€ \ No newline at end of file diff --git a/CL_frontendfixes.md b/CL_frontendfixes.md new file mode 100644 index 0000000..84e7897 --- /dev/null +++ b/CL_frontendfixes.md @@ -0,0 +1,255 @@ +# Frontend Development TODO List + +## ๐Ÿšจ Critical Missing Features (High Priority) + +### 1. Rules Management System +- [ ] **Create Rules page component** (`/src/pages/Rules.jsx`) + - [ ] Natural language textarea editor + - [ ] AI parsing button with loading state + - [ ] JSON preview pane with syntax highlighting + - [ ] Rule validation feedback + - [ ] Save/cancel actions +- [ ] **Create RuleEditor component** (`/src/components/rules/RuleEditor.jsx`) + - [ ] Rich text input with auto-resize + - [ ] Character count and validation + - [ ] Template suggestions dropdown +- [ ] **Create RulePreview component** (`/src/components/rules/RulePreview.jsx`) + - [ ] JSON syntax highlighting (use `react-json-view`) + - [ ] Editable JSON with validation + - [ ] Diff view for rule changes +- [ ] **Create RulesList component** (`/src/components/rules/RulesList.jsx`) + - [ ] Rule set selection dropdown + - [ ] Version history per rule set + - [ ] Delete/duplicate rule sets +- [ ] **API Integration** + - [ ] `POST /api/rules` - Create new rule set + - [ ] `PUT /api/rules/{id}` - Update rule set + - [ ] `GET /api/rules` - List all rule sets + - [ ] `POST /api/rules/{id}/parse` - AI parsing endpoint + +### 2. Plan Generation Workflow +- [ ] **Create PlanGeneration page** (`/src/pages/PlanGeneration.jsx`) + - [ ] Goal selection interface + - [ ] Rule set selection + - [ ] Plan parameters (duration, weekly hours) + - [ ] Progress tracking for AI generation +- [ ] **Create GoalSelector component** (`/src/components/plans/GoalSelector.jsx`) + - [ ] Predefined goal templates + - [ ] Custom goal input + - [ ] Goal validation +- [ ] **Create PlanParameters component** (`/src/components/plans/PlanParameters.jsx`) + - [ ] Duration slider (4-20 weeks) + - [ ] Weekly hours slider (5-15 hours) + - [ ] Difficulty level selection + - [ ] Available days checkboxes +- [ ] **Enhance PlanTimeline component** + - [ ] Week-by-week breakdown + - [ ] Workout details expandable cards + - [ ] Progress tracking indicators + - [ ] Edit individual workouts +- [ ] **API Integration** + - [ ] `POST /api/plans/generate` - Generate new plan + - [ ] `GET /api/plans/{id}/preview` - Preview before saving + - [ ] Plan generation status polling + +### 3. Route Management & Visualization +- [ ] **Enhance RoutesPage** (`/src/pages/RoutesPage.jsx`) + - [ ] Route list with metadata + - [ ] GPX file upload integration + - [ ] Route preview cards + - [ ] Search and filter functionality +- [ ] **Create RouteVisualization component** (`/src/components/routes/RouteVisualization.jsx`) + - [ ] Interactive map (use Leaflet.js) + - [ ] GPX track overlay + - [ ] Elevation profile chart + - [ ] Distance markers +- [ ] **Create RouteMetadata component** (`/src/components/routes/RouteMetadata.jsx`) + - [ ] Distance, elevation gain, grade analysis + - [ ] Estimated time calculations + - [ ] Difficulty rating + - [ ] Notes/description editing +- [ ] **Create SectionManager component** (`/src/components/routes/SectionManager.jsx`) + - [ ] Split routes into sections + - [ ] Section-specific metadata + - [ ] Gear recommendations per section +- [ ] **Dependencies to add** + - [ ] `npm install leaflet react-leaflet` + - [ ] GPX parsing library integration + +### 4. Export/Import System +- [ ] **Create ExportImport page** (`/src/pages/ExportImport.jsx`) + - [ ] Export options (JSON, ZIP) + - [ ] Import validation + - [ ] Bulk operations +- [ ] **Create DataExporter component** (`/src/components/export/DataExporter.jsx`) + - [ ] Selective export (routes, rules, plans) + - [ ] Format selection (JSON, GPX, ZIP) + - [ ] Export progress tracking +- [ ] **Create DataImporter component** (`/src/components/export/DataImporter.jsx`) + - [ ] File validation and preview + - [ ] Conflict resolution interface + - [ ] Import progress tracking +- [ ] **API Integration** + - [ ] `GET /api/export` - Generate export package + - [ ] `POST /api/import` - Import data package + - [ ] `POST /api/import/validate` - Validate before import + +## ๐Ÿ”ง Code Quality & Architecture Improvements + +### 5. Enhanced Error Handling +- [ ] **Create GlobalErrorHandler** (`/src/components/GlobalErrorHandler.jsx`) + - [ ] Centralized error logging + - [ ] User-friendly error messages + - [ ] Retry mechanisms +- [ ] **Improve API error handling** + - [ ] Consistent error response format + - [ ] Network error recovery + - [ ] Timeout handling +- [ ] **Add error boundaries** + - [ ] Page-level error boundaries + - [ ] Component-level error recovery + +### 6. State Management Improvements +- [ ] **Enhance AuthContext** + - [ ] Add user preferences + - [ ] API caching layer + - [ ] Offline capability detection +- [ ] **Create AppStateContext** (`/src/context/AppStateContext.jsx`) + - [ ] Global loading states + - [ ] Toast notifications + - [ ] Modal management +- [ ] **Add React Query** (Optional but recommended) + - [ ] `npm install @tanstack/react-query` + - [ ] API data caching + - [ ] Background refetching + - [ ] Optimistic updates + +### 7. UI/UX Enhancements +- [ ] **Improve responsive design** + - [ ] Better mobile navigation + - [ ] Touch-friendly interactions + - [ ] Responsive charts and maps +- [ ] **Add loading skeletons** + - [ ] Replace generic spinners + - [ ] Component-specific skeletons + - [ ] Progressive loading +- [ ] **Create ConfirmDialog component** (`/src/components/ui/ConfirmDialog.jsx`) + - [ ] Delete confirmations + - [ ] Destructive action warnings + - [ ] Custom confirmation messages +- [ ] **Add keyboard shortcuts** + - [ ] Navigation shortcuts + - [ ] Action shortcuts + - [ ] Help overlay + +## ๐Ÿงช Testing & Quality Assurance + +### 8. Testing Infrastructure +- [ ] **Expand component tests** + - [ ] Rules management tests + - [ ] Plan generation tests + - [ ] Route visualization tests +- [ ] **Add integration tests** + - [ ] API integration tests + - [ ] User workflow tests + - [ ] Error scenario tests +- [ ] **Performance testing** + - [ ] Large dataset handling + - [ ] Chart rendering performance + - [ ] Memory leak detection + +### 9. Development Experience +- [ ] **Add Storybook** (Optional) + - [ ] Component documentation + - [ ] Design system documentation + - [ ] Interactive component testing +- [ ] **Improve build process** + - [ ] Bundle size optimization + - [ ] Dead code elimination + - [ ] Tree shaking verification +- [ ] **Add development tools** + - [ ] React DevTools integration + - [ ] Performance monitoring + - [ ] Bundle analyzer + +## ๐Ÿ“š Documentation & Dependencies + +### 10. Missing Dependencies +```json +{ + "leaflet": "^1.9.4", + "react-leaflet": "^4.2.1", + "react-json-view": "^1.21.3", + "@tanstack/react-query": "^4.32.0", + "react-hook-form": "^7.45.0", + "react-select": "^5.7.4", + "file-saver": "^2.0.5" +} +``` + +### 11. Configuration Files +- [ ] **Create environment config** (`/src/config/index.js`) + - [ ] API endpoints configuration + - [ ] Feature flags + - [ ] Environment-specific settings +- [ ] **Add TypeScript support** (Optional) + - [ ] Convert critical components + - [ ] Add type definitions + - [ ] Improve IDE support + +## ๐Ÿš€ Deployment & Performance + +### 12. Production Readiness +- [ ] **Optimize bundle size** + - [ ] Code splitting implementation + - [ ] Lazy loading for routes + - [ ] Image optimization +- [ ] **Add PWA features** (Optional) + - [ ] Service worker + - [ ] Offline functionality + - [ ] App manifest +- [ ] **Performance monitoring** + - [ ] Core Web Vitals tracking + - [ ] Error tracking integration + - [ ] User analytics + +## ๐Ÿ“… Implementation Priority + +### Phase 1 (Week 1-2): Core Missing Features +1. Rules Management System +2. Plan Generation Workflow +3. Enhanced Route Management + +### Phase 2 (Week 3): Data Management +1. Export/Import System +2. Enhanced Error Handling +3. State Management Improvements + +### Phase 3 (Week 4): Polish & Quality +1. UI/UX Enhancements +2. Testing Infrastructure +3. Performance Optimization + +### Phase 4 (Ongoing): Maintenance +1. Documentation +2. Monitoring +3. User Feedback Integration + +--- + +## ๐ŸŽฏ Success Criteria + +- [ ] All design document workflows implemented +- [ ] 90%+ component test coverage +- [ ] Mobile-responsive design +- [ ] Sub-3s initial page load +- [ ] Accessibility compliance (WCAG 2.1 AA) +- [ ] Cross-browser compatibility (Chrome, Firefox, Safari, Edge) + +## ๐Ÿ“ Notes + +- **Prioritize user-facing features** over internal architecture improvements +- **Test each feature** as you implement it +- **Consider Progressive Web App features** for offline functionality +- **Plan for internationalization** if expanding globally +- **Monitor bundle size** as you add dependencies \ No newline at end of file diff --git a/backend/app/routes/export.py b/backend/app/routes/export.py new file mode 100644 index 0000000..c5c2ade --- /dev/null +++ b/backend/app/routes/export.py @@ -0,0 +1,40 @@ +from fastapi import APIRouter, Query, HTTPException +from fastapi.responses import FileResponse +from app.services.export_service import ExportService +from pathlib import Path +import logging + +router = APIRouter() +logger = logging.getLogger(__name__) + +@router.get("/export") +async def export_data( + types: str = Query(..., description="Comma-separated list of data types to export"), + format: str = Query('json', description="Export format (json, zip, gpx)") +): + valid_types = {'routes', 'rules', 'plans'} + requested_types = set(types.split(',')) + + # Validate requested types + if not requested_types.issubset(valid_types): + raise HTTPException( + status_code=400, + detail=f"Invalid export types. Valid types are: {', '.join(valid_types)}" + ) + + try: + exporter = ExportService() + export_path = await exporter.create_export( + export_types=list(requested_types), + export_format=format + ) + + return FileResponse( + export_path, + media_type="application/zip" if format == 'zip' else "application/json", + filename=f"export_{'_'.join(requested_types)}.{format}" + ) + + except Exception as e: + logger.error(f"Export failed: {str(e)}") + raise HTTPException(status_code=500, detail="Export failed") from e \ No newline at end of file diff --git a/backend/app/routes/import.py b/backend/app/routes/import.py new file mode 100644 index 0000000..7e99147 --- /dev/null +++ b/backend/app/routes/import.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, UploadFile, File, Form, HTTPException +from fastapi.responses import JSONResponse +from app.services.import_service import ImportService +import logging +from typing import Optional + +router = APIRouter() +logger = logging.getLogger(__name__) + +@router.post("/import/validate") +async def validate_import( + file: UploadFile = File(...), +): + try: + importer = ImportService() + validation_result = await importer.validate_import(file) + return JSONResponse(content=validation_result) + except Exception as e: + logger.error(f"Import validation failed: {str(e)}") + raise HTTPException(status_code=400, detail=str(e)) from e + +@router.post("/import") +async def execute_import( + file: UploadFile = File(...), + conflict_resolution: str = Form("skip"), + resolutions: Optional[str] = Form(None), +): + try: + importer = ImportService() + import_result = await importer.execute_import( + file, + conflict_resolution, + resolutions + ) + return JSONResponse(content=import_result) + except Exception as e: + logger.error(f"Import failed: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) from e \ No newline at end of file diff --git a/backend/app/services/export_service.py b/backend/app/services/export_service.py new file mode 100644 index 0000000..3c975fc --- /dev/null +++ b/backend/app/services/export_service.py @@ -0,0 +1,138 @@ +import json +from pathlib import Path +from datetime import datetime +import zipfile +from app.database import SessionLocal +from app.models import Route, Rule, Plan +import tempfile +import logging +import shutil + +logger = logging.getLogger(__name__) + +class ExportService: + def __init__(self): + self.temp_dir = Path(tempfile.gettempdir()) / "cycling_exports" + self.temp_dir.mkdir(exist_ok=True) + + async def create_export(self, export_types, export_format): + """Main export creation entry point""" + export_data = await self._fetch_export_data(export_types) + export_path = self._generate_export_file(export_data, export_format, export_types) + return export_path + + async def _fetch_export_data(self, export_types): + """Fetch data from database based on requested types""" + db = SessionLocal() + try: + data = {} + + if 'routes' in export_types: + routes = db.query(Route).all() + data['routes'] = [self._serialize_route(r) for r in routes] + + if 'rules' in export_types: + rules = db.query(Rule).all() + data['rules'] = [self._serialize_rule(r) for r in rules] + + if 'plans' in export_types: + plans = db.query(Plan).all() + data['plans'] = [self._serialize_plan(p) for p in plans] + + return data + finally: + db.close() + + def _generate_export_file(self, data, format, types): + """Generate the export file in specified format""" + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + base_name = f"export_{'_'.join(types)}_{timestamp}" + + if format == 'json': + return self._create_json_export(data, base_name) + elif format == 'zip': + return self._create_zip_export(data, base_name) + elif format == 'gpx': + return self._create_gpx_export(data, base_name) + else: + raise ValueError(f"Unsupported format: {format}") + + def _create_json_export(self, data, base_name): + """Create single JSON file export""" + export_path = self.temp_dir / f"{base_name}.json" + with open(export_path, 'w') as f: + json.dump(data, f, indent=2) + return export_path + + def _create_zip_export(self, data, base_name): + """Create ZIP archive with JSON and GPX files""" + zip_path = self.temp_dir / f"{base_name}.zip" + with zipfile.ZipFile(zip_path, 'w') as zipf: + # Add JSON data + json_path = self._create_json_export(data, base_name) + zipf.write(json_path, arcname=json_path.name) + + # Add GPX files if exporting routes + if 'routes' in data: + gpx_dir = Path("/app/data/gpx") + for route in data['routes']: + gpx_path = gpx_dir / route['gpx_file_path'] + if gpx_path.exists(): + zipf.write(gpx_path, arcname=f"gpx/{gpx_path.name}") + + return zip_path + + def _create_gpx_export(self, data, base_name): + """Export only GPX files from routes""" + if 'routes' not in data: + raise ValueError("GPX export requires routes to be selected") + + zip_path = self.temp_dir / f"{base_name}.zip" + with zipfile.ZipFile(zip_path, 'w') as zipf: + gpx_dir = Path("/app/data/gpx") + for route in data['routes']: + gpx_path = gpx_dir / route['gpx_file_path'] + if gpx_path.exists(): + zipf.write(gpx_path, arcname=gpx_path.name) + + return zip_path + + def _serialize_route(self, route): + return { + "id": route.id, + "name": route.name, + "description": route.description, + "category": route.category, + "gpx_file_path": route.gpx_file_path, + "created_at": route.created_at.isoformat(), + "updated_at": route.updated_at.isoformat() + } + + def _serialize_rule(self, rule): + return { + "id": rule.id, + "name": rule.name, + "natural_language": rule.natural_language, + "jsonb_rules": rule.jsonb_rules, + "version": rule.version, + "created_at": rule.created_at.isoformat() + } + + def _serialize_plan(self, plan): + return { + "id": plan.id, + "name": plan.name, + "jsonb_plan": plan.jsonb_plan, + "version": plan.version, + "created_at": plan.created_at.isoformat() + } + + def cleanup_temp_files(self): + """Clean up temporary export files older than 1 hour""" + cutoff = datetime.now().timestamp() - 3600 + for file in self.temp_dir.glob("*"): + if file.stat().st_mtime < cutoff: + try: + file.unlink() + except Exception as e: + logger.warning(f"Failed to clean up temp file {file}: {str(e)}") \ No newline at end of file diff --git a/backend/app/services/import_service.py b/backend/app/services/import_service.py new file mode 100644 index 0000000..88c14c1 --- /dev/null +++ b/backend/app/services/import_service.py @@ -0,0 +1,259 @@ +import json +import zipfile +from pathlib import Path +import tempfile +from datetime import datetime +from app.database import SessionLocal +from app.models import Route, Rule, Plan +import shutil +import logging +from sqlalchemy import and_ +from typing import Dict, List + +logger = logging.getLogger(__name__) + +class ImportService: + def __init__(self): + self.temp_dir = Path(tempfile.gettempdir()) / "cycling_imports" + self.temp_dir.mkdir(exist_ok=True) + + async def validate_import(self, file: UploadFile) -> dict: + """Validate import file and detect conflicts""" + try: + # Save uploaded file to temp location + file_path = self.temp_dir / file.filename + with open(file_path, "wb") as f: + shutil.copyfileobj(file.file, f) + + # Extract data based on file type + if file.filename.endswith('.zip'): + data = self._process_zip_import(file_path) + elif file.filename.endswith('.json'): + data = self._process_json_import(file_path) + else: + raise ValueError("Unsupported file format") + + # Detect conflicts + conflicts = [] + if 'routes' in data: + conflicts += self._detect_route_conflicts(data['routes']) + if 'rules' in data: + conflicts += self._detect_rule_conflicts(data['rules']) + if 'plans' in data: + conflicts += self._detect_plan_conflicts(data['plans']) + + return { + "valid": True, + "conflicts": conflicts, + "summary": { + "routes": len(data.get('routes', [])), + "rules": len(data.get('rules', [])), + "plans": len(data.get('plans', [])) + } + } + + except Exception as e: + logger.error(f"Validation error: {str(e)}") + return {"valid": False, "error": str(e)} + + async def execute_import(self, file: UploadFile, + conflict_resolution: str, + resolutions: List[dict]) -> dict: + """Execute the import with specified conflict resolution""" + db = SessionLocal() + try: + db.begin() + + # Process file + file_path = self.temp_dir / file.filename + with open(file_path, "wb") as f: + shutil.copyfileobj(file.file, f) + + if file.filename.endswith('.zip'): + data = self._process_zip_import(file_path) + gpx_files = self._extract_gpx_files(file_path) + elif file.filename.endswith('.json'): + data = self._process_json_import(file_path) + gpx_files = [] + else: + raise ValueError("Unsupported file format") + + # Apply resolutions + resolution_map = {r['id']: r['action'] for r in resolutions} + + # Import data + results = { + "imported": {"routes": 0, "rules": 0, "plans": 0}, + "skipped": {"routes": 0, "rules": 0, "plans": 0}, + "errors": [] + } + + # Import routes + if 'routes' in data: + for route_data in data['routes']: + action = resolution_map.get(route_data['id'], conflict_resolution) + try: + if self._should_import_route(route_data, action, db): + self._import_route(route_data, db) + results["imported"]["routes"] += 1 + else: + results["skipped"]["routes"] += 1 + except Exception as e: + results["errors"].append(f"Route {route_data['id']}: {str(e)}") + + # Import rules + if 'rules' in data: + for rule_data in data['rules']: + action = resolution_map.get(rule_data['id'], conflict_resolution) + try: + if self._should_import_rule(rule_data, action, db): + self._import_rule(rule_data, db) + results["imported"]["rules"] += 1 + else: + results["skipped"]["rules"] += 1 + except Exception as e: + results["errors"].append(f"Rule {rule_data['id']}: {str(e)}") + + # Import plans + if 'plans' in data: + for plan_data in data['plans']: + action = resolution_map.get(plan_data['id'], conflict_resolution) + try: + if self._should_import_plan(plan_data, action, db): + self._import_plan(plan_data, db) + results["imported"]["plans"] += 1 + else: + results["skipped"]["plans"] += 1 + except Exception as e: + results["errors"].append(f"Plan {plan_data['id']}: {str(e)}") + + # Save GPX files + if gpx_files: + gpx_dir = Path("/app/data/gpx") + for gpx in gpx_files: + shutil.move(gpx, gpx_dir / gpx.name) + + db.commit() + return results + + except Exception as e: + db.rollback() + logger.error(f"Import failed: {str(e)}") + return {"error": str(e)} + finally: + db.close() + self._cleanup_temp_files() + + def _process_zip_import(self, file_path: Path) -> dict: + """Extract and process ZIP file import""" + data = {} + with zipfile.ZipFile(file_path, 'r') as zipf: + # Find data.json + json_files = [f for f in zipf.namelist() if f.endswith('.json')] + if not json_files: + raise ValueError("No JSON data found in ZIP file") + + with zipf.open(json_files[0]) as f: + data = json.load(f) + + return data + + def _process_json_import(self, file_path: Path) -> dict: + """Process JSON file import""" + with open(file_path) as f: + return json.load(f) + + def _extract_gpx_files(self, file_path: Path) -> List[Path]: + """Extract GPX files from ZIP archive""" + gpx_files = [] + extract_dir = self.temp_dir / "gpx" + extract_dir.mkdir(exist_ok=True) + + with zipfile.ZipFile(file_path, 'r') as zipf: + for file in zipf.namelist(): + if file.startswith('gpx/') and file.endswith('.gpx'): + zipf.extract(file, extract_dir) + gpx_files.append(extract_dir / file) + + return gpx_files + + def _detect_route_conflicts(self, routes: List[dict]) -> List[dict]: + conflicts = [] + db = SessionLocal() + try: + for route in routes: + existing = db.query(Route).filter( + (Route.id == route['id']) | + (Route.name == route['name']) + ).first() + + if existing: + conflict = { + "type": "route", + "id": route['id'], + "name": route['name'], + "existing_version": existing.updated_at, + "import_version": datetime.fromisoformat(route['updated_at']), + "resolution_options": ["overwrite", "rename", "skip"] + } + conflicts.append(conflict) + finally: + db.close() + return conflicts + + def _should_import_route(self, route_data: dict, action: str, db) -> bool: + existing = db.query(Route).filter( + (Route.id == route_data['id']) | + (Route.name == route_data['name']) + ).first() + + if not existing: + return True + + if action == 'overwrite': + return True + elif action == 'rename': + route_data['name'] = f"{route_data['name']} (Imported)" + return True + elif action == 'skip': + return False + + return False + + def _import_route(self, route_data: dict, db): + """Import a single route""" + existing = db.query(Route).get(route_data['id']) + if existing: + # Update existing route + existing.name = route_data['name'] + existing.description = route_data['description'] + existing.category = route_data['category'] + existing.gpx_file_path = route_data['gpx_file_path'] + existing.updated_at = datetime.fromisoformat(route_data['updated_at']) + else: + # Create new route + route = Route( + id=route_data['id'], + name=route_data['name'], + description=route_data['description'], + category=route_data['category'], + gpx_file_path=route_data['gpx_file_path'], + created_at=datetime.fromisoformat(route_data['created_at']), + updated_at=datetime.fromisoformat(route_data['updated_at']) + ) + db.add(route) + + # Similar methods for rules and plans would follow... + + def _cleanup_temp_files(self): + """Clean up temporary files older than 1 hour""" + cutoff = datetime.now().timestamp() - 3600 + for file in self.temp_dir.glob("*"): + if file.stat().st_mtime < cutoff: + try: + if file.is_dir(): + shutil.rmtree(file) + else: + file.unlink() + except Exception as e: + logger.warning(f"Failed to clean temp file {file}: {str(e)}") \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 2dbc303..948c893 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,11 +26,14 @@ services: start_period: 40s frontend: - build: ./frontend + build: + context: ./frontend + args: + - REACT_APP_API_URL=http://backend:8000 ports: - "8888:80" environment: - - REACT_APP_API_URL=http://backend:8000 + - REACT_APP_CONTAINER_API_URL=http://backend:8000 - REACT_APP_API_KEY=${API_KEY} db: diff --git a/export_import_api_spec.md b/export_import_api_spec.md new file mode 100644 index 0000000..eb9fb77 --- /dev/null +++ b/export_import_api_spec.md @@ -0,0 +1,91 @@ +# Export/Import API Specification + +## Export Endpoint +`GET /api/export` + +### Parameters (query string) +- `types` (required): Comma-separated list of data types to export + - Valid values: `routes`, `rules`, `plans`, `all` +- `format` (required): Export format + - `json`: Single JSON file + - `zip`: ZIP archive with separate files + - `gpx`: Only GPX files (routes only) + +### Response +- `200 OK` with file download +- `400 Bad Request` for invalid parameters +- `500 Internal Server Error` for export failures + +### Example +```http +GET /api/export?types=routes,plans&format=zip +``` + +--- + +## Import Validation +`POST /api/import/validate` + +### Request +- Multipart form with `file` field containing import data + +### Response +```json +{ + "valid": true, + "conflicts": [ + { + "type": "route", + "id": 123, + "name": "Mountain Loop", + "existing_version": 2, + "import_version": 3, + "resolution_options": ["overwrite", "rename", "skip"] + } + ], + "summary": { + "routes": 15, + "rules": 3, + "plans": 2 + } +} +``` + +--- + +## Import Execution +`POST /api/import` + +### Request +- Multipart form with: + - `file`: Import data file + - `conflict_resolution`: Global strategy (overwrite, skip, rename) + - `resolutions`: JSON array of specific resolutions (optional) + ```json + [{"type": "route", "id": 123, "action": "overwrite"}] + ``` + +### Response +```json +{ + "imported": { + "routes": 12, + "rules": 3, + "plans": 2 + }, + "skipped": { + "routes": 3, + "rules": 0, + "plans": 0 + }, + "errors": [] +} +``` + +### Status Codes +- `200 OK`: Import completed +- `202 Accepted`: Import in progress (async) +- `400 Bad Request`: Invalid input +- `409 Conflict`: Unresolved conflicts + +--- \ No newline at end of file diff --git a/export_import_frontend_spec.md b/export_import_frontend_spec.md new file mode 100644 index 0000000..db29970 --- /dev/null +++ b/export_import_frontend_spec.md @@ -0,0 +1,221 @@ +# Export/Import Frontend Implementation + +## File Structure +``` +src/pages/ + ExportImport.jsx # Main page + +src/components/export/ + DataExporter.jsx # Export functionality + DataImporter.jsx # Import functionality + ConflictDialog.jsx # Conflict resolution UI + ImportSummary.jsx # Post-import report +``` + +## Component Specifications + +### ExportImport.jsx +```jsx +import { useState } from 'react'; +import DataExporter from '../components/export/DataExporter'; +import DataImporter from '../components/export/DataImporter'; + +export default function ExportImportPage() { + const [activeTab, setActiveTab] = useState('export'); + + return ( +
+
+ + +
+ + {activeTab === 'export' ? : } +
+ ); +} +``` + +### DataExporter.jsx +```jsx +import { useState } from 'react'; + +const EXPORT_TYPES = [ + { id: 'routes', label: 'Routes' }, + { id: 'rules', label: 'Training Rules' }, + { id: 'plans', label: 'Training Plans' } +]; + +const EXPORT_FORMATS = [ + { id: 'json', label: 'JSON' }, + { id: 'zip', label: 'ZIP Archive' }, + { id: 'gpx', label: 'GPX Files' } +]; + +export default function DataExporter() { + const [selectedTypes, setSelectedTypes] = useState([]); + const [selectedFormat, setSelectedFormat] = useState('json'); + const [isExporting, setIsExporting] = useState(false); + const [progress, setProgress] = useState(0); + + const handleExport = async () => { + setIsExporting(true); + // API call to /api/export?types=...&format=... + // Track progress and trigger download + }; + + return ( +
+

Export Data

+ +
+

Select Data to Export

+ {EXPORT_TYPES.map(type => ( + + ))} +
+ +
+

Export Format

+ +
+ + +
+ ); +} +``` + +### DataImporter.jsx +```jsx +import { useState } from 'react'; +import ConflictDialog from './ConflictDialog'; + +export default function DataImporter() { + const [file, setFile] = useState(null); + const [validation, setValidation] = useState(null); + const [isImporting, setIsImporting] = useState(false); + const [showConflictDialog, setShowConflictDialog] = useState(false); + + const handleFileUpload = (e) => { + const file = e.target.files[0]; + setFile(file); + // Call /api/import/validate + // Set validation results + }; + + const handleImport = () => { + if (validation?.conflicts?.length > 0) { + setShowConflictDialog(true); + } else { + startImport(); + } + }; + + const startImport = (resolutions = []) => { + setIsImporting(true); + // Call /api/import with conflict resolutions + }; + + return ( +
+

Import Data

+ + + + {validation && ( +
+

Validation Results

+

Found: {validation.summary.routes} routes, + {validation.summary.rules} rules, + {validation.summary.plans} plans

+ {validation.conflicts.length > 0 && ( +

โš ๏ธ {validation.conflicts.length} conflicts detected

+ )} +
+ )} + + + + {showConflictDialog && ( + setShowConflictDialog(false)} + /> + )} +
+ ); +} +``` + +### ConflictDialog.jsx +```jsx +export default function ConflictDialog({ conflicts, onResolve, onCancel }) { + const [resolutions, setResolutions] = useState({}); + + const handleResolution = (id, action) => { + setResolutions(prev => ({ ...prev, [id]: action })); + }; + + const applyResolutions = () => { + const resolutionList = Object.entries(resolutions).map(([id, action]) => ({ + id, + action + })); + onResolve(resolutionList); + }; + + return ( +
+

Resolve Conflicts

+
+ {conflicts.map(conflict => ( +
+

{conflict.name} ({conflict.type})

+

Existing version: {conflict.existing_version}

+

Import version: {conflict.import_version}

+ +
+ ))} +
+
+ + +
+
+ ); +} +``` + +## Dependencies to Install +```bash +npm install react-dropzone react-json-view file-saver \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 06f0dd5..da067ea 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,6 +1,10 @@ # Stage 1: Build application FROM node:20-alpine AS builder +# Allow environment variables to be passed at build time +ARG REACT_APP_API_URL +ENV REACT_APP_API_URL=$REACT_APP_API_URL + WORKDIR /app # Copy package manifests first for optimal caching @@ -9,7 +13,7 @@ COPY package.json package-lock.json* ./ # Clean cache and install dependencies RUN npm cache clean --force && \ export NODE_OPTIONS="--max-old-space-size=1024" && \ - npm install --include=dev + npm install --omit=dev --legacy-peer-deps # Copy source files COPY . . diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bc4fc1d..77af149 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,9 +10,20 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "axios": "^1.7.2", + "date-fns": "^3.6.0", + "gpx-parse": "^0.10.4", + "leaflet": "^1.9.4", "next": "14.2.3", "react": "18.2.0", "react-dom": "18.2.0", + "react-hook-form": "^7.45.0", + "react-icons": "^5.5.0", + "react-json-view": "^1.21.3", + "react-leaflet": "^5.0.0", + "react-router-dom": "^6.22.3", + "react-select": "^5.7.4", + "react-toastify": "^10.0.4", "recharts": "2.8.0" }, "devDependencies": { @@ -762,6 +773,28 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -1608,6 +1641,24 @@ "node": ">=14" } }, + "node_modules/@react-leaflet/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", + "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1658,36 +1709,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/dom/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "peer": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/@testing-library/jest-dom": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.2.tgz", @@ -2021,6 +2042,14 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.26.0.tgz", @@ -2755,6 +2784,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -2773,8 +2807,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/available-typed-arrays": { "version": "1.0.7", @@ -2800,6 +2833,16 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -2948,6 +2991,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base16": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz", + "integrity": "sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -3050,7 +3098,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -3218,6 +3265,14 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -3256,7 +3311,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3409,6 +3463,14 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3644,6 +3706,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -3770,21 +3841,10 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -3858,7 +3918,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -3990,7 +4049,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -3999,7 +4057,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -4055,7 +4112,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -4067,7 +4123,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -4725,6 +4780,33 @@ "bser": "2.1.1" } }, + "node_modules/fbemitter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-3.0.0.tgz", + "integrity": "sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==", + "dependencies": { + "fbjs": "^3.0.0" + } + }, + "node_modules/fbjs": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz", + "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==", + "dependencies": { + "cross-fetch": "^3.1.5", + "fbjs-css-vars": "^1.0.0", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^1.0.35" + } + }, + "node_modules/fbjs-css-vars": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", + "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4790,6 +4872,37 @@ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true }, + "node_modules/flux": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/flux/-/flux-4.0.4.tgz", + "integrity": "sha512-NCj3XlayA2UsapRpM7va6wU1+9rE5FIL7qoMcmxWHRzbp0yujihMBm9BBHZ1MDIk5h5o2Bl6eGiCe8rYELAmYw==", + "dependencies": { + "fbemitter": "^3.0.0", + "fbjs": "^3.0.1" + }, + "peerDependencies": { + "react": "^15.0.2 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -4825,7 +4938,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -4916,7 +5028,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -4949,7 +5060,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -5112,7 +5222,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -5120,6 +5229,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gpx-parse": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/gpx-parse/-/gpx-parse-0.10.4.tgz", + "integrity": "sha512-4PUA2Hzp4m5EjDEa4YB1CKWh5Cjm1cgJuDe+nL6B77DgtFuYR7VtvBbmvbwjx6M40iA96q2BaKyD8/lraez2xw==", + "dependencies": { + "xml2js": "^0.4.4" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -5183,7 +5300,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -5195,7 +5311,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -7033,6 +7148,11 @@ "node": ">=0.10" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -7080,6 +7200,16 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.curry": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz", + "integrity": "sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==" + }, + "node_modules/lodash.flow": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz", + "integrity": "sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -7140,11 +7270,15 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "engines": { "node": ">= 0.4" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -7177,7 +7311,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -7186,7 +7319,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -7334,6 +7466,44 @@ } } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -7868,6 +8038,14 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dependencies": { + "asap": "~2.0.3" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -7891,6 +8069,11 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -7912,6 +8095,11 @@ "node": ">=6" } }, + "node_modules/pure-color": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz", + "integrity": "sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==" + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -7965,6 +8153,17 @@ "node": ">=0.10.0" } }, + "node_modules/react-base16-styling": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.6.0.tgz", + "integrity": "sha512-yvh/7CArceR/jNATXOKDlvTnPKPmGZz7zsenQ3jUwLzHkNUR0CvY3yGYJbWJ/nnxsL8Sgmt5cO3/SILVuPO6TQ==", + "dependencies": { + "base16": "^1.0.0", + "lodash.curry": "^4.0.1", + "lodash.flow": "^3.3.0", + "pure-color": "^1.2.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -7977,11 +8176,62 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.62.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", + "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-json-view": { + "version": "1.21.3", + "resolved": "https://registry.npmjs.org/react-json-view/-/react-json-view-1.21.3.tgz", + "integrity": "sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==", + "dependencies": { + "flux": "^4.0.1", + "react-base16-styling": "^0.6.0", + "react-lifecycles-compat": "^3.0.4", + "react-textarea-autosize": "^8.3.2" + }, + "peerDependencies": { + "react": "^17.0.0 || ^16.3.0 || ^15.5.4", + "react-dom": "^17.0.0 || ^16.3.0 || ^15.5.4" + } + }, + "node_modules/react-leaflet": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", + "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", + "dependencies": { + "@react-leaflet/core": "^3.0.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", @@ -7999,6 +8249,80 @@ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-router": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "dependencies": { + "@remix-run/router": "1.23.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "dependencies": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-select": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz", + "integrity": "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-select/node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/react-select/node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/react-smooth": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.5.tgz", @@ -8013,6 +8337,34 @@ "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-textarea-autosize": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", + "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-toastify": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.6.tgz", + "integrity": "sha512-yYjp+omCDf9lhZcrZHKbSq7YMuK0zcYkDFTzfRFgTXkTFHZ1ToxwAonzA4JI5CxA91JpjFLmwEsZEgfYfOqI1A==", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/react-transition-group": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", @@ -8333,6 +8685,11 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -8411,6 +8768,11 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9208,6 +9570,31 @@ "node": ">=14.17" } }, + "node_modules/ua-parser-js": { + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", + "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -9324,6 +9711,48 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-composed-ref": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", + "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz", + "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -9687,6 +10116,26 @@ "node": ">=12" } }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6930ec7..d4f9be5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,10 +16,17 @@ "@emotion/styled": "^11.14.1", "axios": "^1.7.2", "date-fns": "^3.6.0", + "@tmcw/togeojson": "^7.1.2", + "leaflet": "^1.9.4", "next": "14.2.3", "react": "18.2.0", "react-dom": "18.2.0", + "react-hook-form": "^7.45.0", + "react-icons": "^5.5.0", + "react-json-tree": "^0.20.0", + "react-leaflet": "^4.2.1", "react-router-dom": "^6.22.3", + "react-select": "^5.7.4", "react-toastify": "^10.0.4", "recharts": "2.8.0" }, diff --git a/frontend/src/components/Navigation.jsx b/frontend/src/components/Navigation.jsx index de87fbe..1dccdf1 100644 --- a/frontend/src/components/Navigation.jsx +++ b/frontend/src/components/Navigation.jsx @@ -23,6 +23,12 @@ const Navigation = () => { > Plans + + Rules + { + const [formData, setFormData] = useState({ + type: '', + duration_minutes: 0, + intensity: '', + description: '' + }); + + useEffect(() => { + if (workout) { + setFormData({ + type: workout.type || '', + duration_minutes: workout.duration_minutes || 0, + intensity: workout.intensity || '', + description: workout.description || '' + }); + } + }, [workout]); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + onSave(formData); + }; + + if (!workout) return null; + + return ( +
+
+

Edit Workout

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