From db5cb86d3ab695269a68ba93c9df6ff0cdce0731 Mon Sep 17 00:00:00 2001 From: maziggy Date: Sun, 28 Dec 2025 12:04:33 +0100 Subject: [PATCH] - Project page enhancements and bug fixes - Add filament color swatches to project cards showing colors from assigned archives - Add "Hide done" toggle to filter completed BOM items - Include projects in backup/restore (with BOM items and attachments) - Add file type validation for project attachments - Enhance project card design with gradients, shadows, and glow effects - Improve layout spacing on project list and detail pages - Replace browser confirm dialogs with styled confirmation modals - Fix attachment uploads not persisting (SQLAlchemy JSON column mutation) --- CHANGELOG.md | 8 + README.md | 8 - backend/app/api/routes/projects.py | 161 +- backend/app/api/routes/settings.py | 397 +- backend/app/core/database.py | 24 + backend/app/models/project_bom.py | 22 +- backend/app/schemas/project.py | 20 +- frontend/src/api/client.ts | 22 +- frontend/src/components/BackupModal.tsx | 18 +- frontend/src/pages/ProjectDetailPage.tsx | 339 +- frontend/src/pages/ProjectsPage.tsx | 150 +- static/assets/index-BonMKEhM.js | 8614 ++++++++++++++++++++++ static/assets/index-CUXYdb1N.css | 1 - static/assets/index-DGJjegsB.css | 1 + static/assets/index-DpxuzRSm.js | 8614 ---------------------- static/index.html | 4 +- 16 files changed, 9477 insertions(+), 8926 deletions(-) create mode 100644 static/assets/index-BonMKEhM.js delete mode 100644 static/assets/index-CUXYdb1N.css create mode 100644 static/assets/index-DGJjegsB.css delete mode 100644 static/assets/index-DpxuzRSm.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 698ca2ae1..a0f496a99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,21 @@ All notable changes to Bambuddy will be documented in this file. - **Docker printer discovery** - Subnet scanning for discovering printers when running in Docker with `network_mode: host`. Automatically detects Docker environment and shows subnet input field in Add Printer dialog. - **Printer model mapping** - Discovery now shows friendly model names (X1C, H2D, P1S) instead of raw SSDP codes (BL-P001, O1D, C11). - **Discovery API tests** - Comprehensive test coverage for discovery endpoints. +- **Project filament colors** - Project cards now display filament color swatches from assigned archives. +- **BOM filter** - Hide completed BOM items with "Hide done" toggle on project detail page. +- **Projects in backup/restore** - Projects, BOM items, and attachments now included in database backup/restore. +- **Attachment file validation** - File type validation for project attachments (images, documents, 3D files, archives, scripts, configs). ### Changed - **GitHub issue template** - Added mandatory printer firmware version field and LAN-only mode checkbox for better bug reports. - **Docker compose** - Clearer comments explaining `network_mode: host` requirement for printer discovery and camera streaming. +- **Project card design** - Enhanced visual polish with gradients, shadows, and glow effects on hover. +- **Project page layout** - Improved spacing and padding on project list and detail pages. +- **Delete confirmations** - Replaced browser confirm dialogs with styled confirmation modals. ### Fixed - **Notification module** - Fixed bug where notifications were sent even when printer was offline. +- **Attachment uploads** - Fixed file attachments not persisting due to SQLAlchemy JSON column mutation detection. ## [0.1.5] - 2025-12-19 diff --git a/README.md b/README.md index 01fc98d21..f52034b4d 100644 --- a/README.md +++ b/README.md @@ -97,14 +97,6 @@ - Print time accuracy stats - File manager for printer storage -### 🎛️ Printer Control -- AMS/AMS-HT temperature & humidity monitoring -- Chamber temperature & light control -- Speed profiles & fan controls -- AI detection modules (spaghetti, first layer) -- Automated calibration (bed level, vibration) -- Dual nozzle support - diff --git a/backend/app/api/routes/projects.py b/backend/app/api/routes/projects.py index 0d4337cc6..c43a4a575 100644 --- a/backend/app/api/routes/projects.py +++ b/backend/app/api/routes/projects.py @@ -98,7 +98,7 @@ async def compute_project_stats(db: AsyncSession, project_id: int, target_count: bom_result = await db.execute( select( func.count(ProjectBOMItem.id).label("total"), - func.sum(case((ProjectBOMItem.quantity_printed >= ProjectBOMItem.quantity_needed, 1), else_=0)).label( + func.sum(case((ProjectBOMItem.quantity_acquired >= ProjectBOMItem.quantity_needed, 1), else_=0)).label( "completed" ), ).where(ProjectBOMItem.project_id == project_id) @@ -183,6 +183,8 @@ async def list_projects( print_name=a.print_name, thumbnail_path=a.thumbnail_path, status=a.status, + filament_type=a.filament_type, + filament_color=a.filament_color, ) for a in archives ] @@ -342,9 +344,11 @@ async def create_project_from_template( project_id=project.id, name=item.name, quantity_needed=item.quantity_needed, - quantity_printed=0, + quantity_acquired=0, + unit_price=item.unit_price, + sourcing_url=item.sourcing_url, stl_filename=item.stl_filename, - notes=item.notes, + remarks=item.remarks, sort_order=item.sort_order, ) db.add(new_item) @@ -691,6 +695,63 @@ def get_project_attachments_dir(project_id: int) -> Path: return base_dir / "projects" / str(project_id) / "attachments" +# Allowed file extensions for attachments +ALLOWED_ATTACHMENT_EXTENSIONS = { + # Images + ".jpg", + ".jpeg", + ".png", + ".gif", + ".webp", + ".svg", + ".bmp", + ".ico", + # Documents + ".pdf", + ".doc", + ".docx", + ".xls", + ".xlsx", + ".ppt", + ".pptx", + ".odt", + ".ods", + ".odp", + ".txt", + ".rtf", + ".csv", + ".md", + # 3D/CAD files + ".stl", + ".obj", + ".3mf", + ".step", + ".stp", + ".iges", + ".igs", + ".f3d", + ".scad", + # Archives + ".zip", + ".rar", + ".7z", + ".tar", + ".gz", + # Code/scripts (for Klipper macros, scripts, etc.) + ".py", + ".sh", + ".cfg", + ".conf", + ".gcode", + ".ini", + # Other common formats + ".json", + ".xml", + ".yaml", + ".yml", +} + + @router.post("/{project_id}/attachments") async def upload_attachment( project_id: int, @@ -698,19 +759,28 @@ async def upload_attachment( db: AsyncSession = Depends(get_db), ): """Upload an attachment to a project.""" + logger.info(f"=== UPLOAD START: {file.filename} for project {project_id} ===") + # Verify project exists result = await db.execute(select(Project).where(Project.id == project_id)) project = result.scalar_one_or_none() if not project: raise HTTPException(status_code=404, detail="Project not found") + # Validate file extension + original_name = file.filename or "unknown" + ext = os.path.splitext(original_name)[1].lower() + if ext not in ALLOWED_ATTACHMENT_EXTENSIONS: + raise HTTPException( + status_code=400, + detail=f"File type '{ext}' not supported. Allowed: images, PDFs, documents, STL, 3MF, archives.", + ) + # Create attachments directory attachments_dir = get_project_attachments_dir(project_id) attachments_dir.mkdir(parents=True, exist_ok=True) # Generate unique filename - original_name = file.filename or "unknown" - ext = os.path.splitext(original_name)[1] unique_filename = f"{uuid.uuid4().hex}{ext}" file_path = attachments_dir / unique_filename @@ -719,30 +789,43 @@ async def upload_attachment( with open(file_path, "wb") as f: content = await file.read() f.write(content) + logger.info(f"=== FILE SAVED: {file_path}, size: {len(content)} ===") except Exception as e: logger.error(f"Failed to save attachment: {e}") raise HTTPException(status_code=500, detail="Failed to save attachment") # Update project attachments JSON - attachments = project.attachments or [] - attachments.append( - { - "filename": unique_filename, - "original_name": original_name, - "size": len(content), - "uploaded_at": datetime.now().isoformat(), - } - ) + attachments = list(project.attachments or []) + new_attachment = { + "filename": unique_filename, + "original_name": original_name, + "size": len(content), + "uploaded_at": datetime.now().isoformat(), + } + attachments.append(new_attachment) + + # Simple ORM update project.attachments = attachments + db.add(project) # Explicitly add to session + + logger.info(f"=== BEFORE COMMIT: {len(attachments)} attachments ===") await db.flush() - await db.refresh(project) + await db.commit() + + logger.info("=== AFTER COMMIT ===") + + # Verify by re-querying + result = await db.execute(select(Project).where(Project.id == project_id)) + fresh_project = result.scalar_one() + + logger.info(f"=== VERIFIED: {len(fresh_project.attachments or [])} attachments ===") return { "status": "success", "filename": unique_filename, "original_name": original_name, - "attachments": project.attachments, + "attachments": fresh_project.attachments, } @@ -854,13 +937,15 @@ async def list_bom_items( project_id=item.project_id, name=item.name, quantity_needed=item.quantity_needed, - quantity_printed=item.quantity_printed, + quantity_acquired=item.quantity_acquired, + unit_price=item.unit_price, + sourcing_url=item.sourcing_url, archive_id=item.archive_id, archive_name=archive_name, stl_filename=item.stl_filename, - notes=item.notes, + remarks=item.remarks, sort_order=item.sort_order, - is_complete=item.quantity_printed >= item.quantity_needed, + is_complete=item.quantity_acquired >= item.quantity_needed, created_at=item.created_at, updated_at=item.updated_at, ) @@ -891,9 +976,11 @@ async def create_bom_item( project_id=project_id, name=data.name, quantity_needed=data.quantity_needed, + unit_price=data.unit_price, + sourcing_url=data.sourcing_url, archive_id=data.archive_id, stl_filename=data.stl_filename, - notes=data.notes, + remarks=data.remarks, sort_order=max_order + 1, ) db.add(item) @@ -911,13 +998,15 @@ async def create_bom_item( project_id=item.project_id, name=item.name, quantity_needed=item.quantity_needed, - quantity_printed=item.quantity_printed, + quantity_acquired=item.quantity_acquired, + unit_price=item.unit_price, + sourcing_url=item.sourcing_url, archive_id=item.archive_id, archive_name=archive_name, stl_filename=item.stl_filename, - notes=item.notes, + remarks=item.remarks, sort_order=item.sort_order, - is_complete=item.quantity_printed >= item.quantity_needed, + is_complete=item.quantity_acquired >= item.quantity_needed, created_at=item.created_at, updated_at=item.updated_at, ) @@ -946,14 +1035,18 @@ async def update_bom_item( item.name = data.name if data.quantity_needed is not None: item.quantity_needed = data.quantity_needed - if data.quantity_printed is not None: - item.quantity_printed = data.quantity_printed + if data.quantity_acquired is not None: + item.quantity_acquired = data.quantity_acquired + if data.unit_price is not None: + item.unit_price = data.unit_price if data.unit_price != 0 else None + if data.sourcing_url is not None: + item.sourcing_url = data.sourcing_url if data.sourcing_url else None if data.archive_id is not None: item.archive_id = data.archive_id if data.archive_id != 0 else None if data.stl_filename is not None: item.stl_filename = data.stl_filename if data.stl_filename else None - if data.notes is not None: - item.notes = data.notes if data.notes else None + if data.remarks is not None: + item.remarks = data.remarks if data.remarks else None await db.flush() await db.refresh(item) @@ -969,13 +1062,15 @@ async def update_bom_item( project_id=item.project_id, name=item.name, quantity_needed=item.quantity_needed, - quantity_printed=item.quantity_printed, + quantity_acquired=item.quantity_acquired, + unit_price=item.unit_price, + sourcing_url=item.sourcing_url, archive_id=item.archive_id, archive_name=archive_name, stl_filename=item.stl_filename, - notes=item.notes, + remarks=item.remarks, sort_order=item.sort_order, - is_complete=item.quantity_printed >= item.quantity_needed, + is_complete=item.quantity_acquired >= item.quantity_needed, created_at=item.created_at, updated_at=item.updated_at, ) @@ -1041,9 +1136,11 @@ async def create_template_from_project( project_id=template.id, name=item.name, quantity_needed=item.quantity_needed, - quantity_printed=0, + quantity_acquired=0, + unit_price=item.unit_price, + sourcing_url=item.sourcing_url, stl_filename=item.stl_filename, - notes=item.notes, + remarks=item.remarks, sort_order=item.sort_order, ) db.add(new_item) diff --git a/backend/app/api/routes/settings.py b/backend/app/api/routes/settings.py index 64eb2766e..2f29a323f 100644 --- a/backend/app/api/routes/settings.py +++ b/backend/app/api/routes/settings.py @@ -3,28 +3,28 @@ import json import zipfile from datetime import datetime from pathlib import Path -from typing import Optional -from fastapi import APIRouter, Depends, UploadFile, File, Query +from fastapi import APIRouter, Depends, File, Query, UploadFile from fastapi.responses import JSONResponse, StreamingResponse -from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from backend.app.core.config import settings as app_settings from backend.app.core.database import get_db -from backend.app.models.settings import Settings -from backend.app.models.notification import NotificationProvider -from backend.app.models.notification_template import NotificationTemplate -from backend.app.models.smart_plug import SmartPlug -from backend.app.models.printer import Printer -from backend.app.models.filament import Filament -from backend.app.models.maintenance import MaintenanceType, PrinterMaintenance, MaintenanceHistory from backend.app.models.archive import PrintArchive from backend.app.models.external_link import ExternalLink +from backend.app.models.filament import Filament +from backend.app.models.maintenance import MaintenanceType +from backend.app.models.notification import NotificationProvider +from backend.app.models.notification_template import NotificationTemplate +from backend.app.models.printer import Printer +from backend.app.models.project import Project +from backend.app.models.project_bom import ProjectBOMItem +from backend.app.models.settings import Settings +from backend.app.models.smart_plug import SmartPlug from backend.app.schemas.settings import AppSettings, AppSettingsUpdate from backend.app.services.printer_manager import printer_manager -from backend.app.services.spoolman import init_spoolman_client, get_spoolman_client - +from backend.app.services.spoolman import init_spoolman_client router = APIRouter(prefix="/settings", tags=["settings"]) @@ -63,7 +63,14 @@ async def get_settings(db: AsyncSession = Depends(get_db)): for setting in db_settings: if setting.key in settings_dict: # Parse the value based on the expected type - if setting.key in ["auto_archive", "save_thumbnails", "capture_finish_photo", "spoolman_enabled", "check_updates", "telemetry_enabled"]: + if setting.key in [ + "auto_archive", + "save_thumbnails", + "capture_finish_photo", + "spoolman_enabled", + "check_updates", + "telemetry_enabled", + ]: settings_dict[setting.key] = setting.value.lower() == "true" elif setting.key in ["default_filament_cost", "energy_cost_per_kwh", "ams_temp_good", "ams_temp_fair"]: settings_dict[setting.key] = float(setting.value) @@ -173,6 +180,7 @@ async def export_backup( include_filaments: bool = Query(False, description="Include filament inventory"), include_maintenance: bool = Query(False, description="Include maintenance types and records"), include_archives: bool = Query(False, description="Include print archive metadata"), + include_projects: bool = Query(False, description="Include projects with BOM items"), include_access_codes: bool = Query(False, description="Include printer access codes (security risk!)"), ): """Export selected data as JSON backup.""" @@ -195,27 +203,29 @@ async def export_backup( providers = result.scalars().all() backup["notification_providers"] = [] for p in providers: - backup["notification_providers"].append({ - "name": p.name, - "provider_type": p.provider_type, - "enabled": p.enabled, - "config": json.loads(p.config) if isinstance(p.config, str) else p.config, - "on_print_start": p.on_print_start, - "on_print_complete": p.on_print_complete, - "on_print_failed": p.on_print_failed, - "on_print_stopped": p.on_print_stopped, - "on_print_progress": p.on_print_progress, - "on_printer_offline": p.on_printer_offline, - "on_printer_error": p.on_printer_error, - "on_filament_low": p.on_filament_low, - "on_maintenance_due": p.on_maintenance_due, - "quiet_hours_enabled": p.quiet_hours_enabled, - "quiet_hours_start": p.quiet_hours_start, - "quiet_hours_end": p.quiet_hours_end, - "daily_digest_enabled": getattr(p, 'daily_digest_enabled', False), - "daily_digest_time": getattr(p, 'daily_digest_time', None), - "printer_id": getattr(p, 'printer_id', None), - }) + backup["notification_providers"].append( + { + "name": p.name, + "provider_type": p.provider_type, + "enabled": p.enabled, + "config": json.loads(p.config) if isinstance(p.config, str) else p.config, + "on_print_start": p.on_print_start, + "on_print_complete": p.on_print_complete, + "on_print_failed": p.on_print_failed, + "on_print_stopped": p.on_print_stopped, + "on_print_progress": p.on_print_progress, + "on_printer_offline": p.on_printer_offline, + "on_printer_error": p.on_printer_error, + "on_filament_low": p.on_filament_low, + "on_maintenance_due": p.on_maintenance_due, + "quiet_hours_enabled": p.quiet_hours_enabled, + "quiet_hours_start": p.quiet_hours_start, + "quiet_hours_end": p.quiet_hours_end, + "daily_digest_enabled": getattr(p, "daily_digest_enabled", False), + "daily_digest_time": getattr(p, "daily_digest_time", None), + "printer_id": getattr(p, "printer_id", None), + } + ) backup["included"].append("notification_providers") # Notification templates @@ -224,13 +234,15 @@ async def export_backup( templates = result.scalars().all() backup["notification_templates"] = [] for t in templates: - backup["notification_templates"].append({ - "event_type": t.event_type, - "name": t.name, - "title_template": t.title_template, - "body_template": t.body_template, - "is_default": t.is_default, - }) + backup["notification_templates"].append( + { + "event_type": t.event_type, + "name": t.name, + "title_template": t.title_template, + "body_template": t.body_template, + "is_default": t.is_default, + } + ) backup["included"].append("notification_templates") # Smart plugs @@ -239,25 +251,27 @@ async def export_backup( plugs = result.scalars().all() backup["smart_plugs"] = [] for plug in plugs: - backup["smart_plugs"].append({ - "name": plug.name, - "ip_address": plug.ip_address, - "printer_id": plug.printer_id, - "enabled": plug.enabled, - "auto_on": plug.auto_on, - "auto_off": plug.auto_off, - "off_delay_mode": plug.off_delay_mode, - "off_delay_minutes": plug.off_delay_minutes, - "off_temp_threshold": plug.off_temp_threshold, - "username": plug.username, - "password": plug.password, - "power_alert_enabled": plug.power_alert_enabled, - "power_alert_high": plug.power_alert_high, - "power_alert_low": plug.power_alert_low, - "schedule_enabled": plug.schedule_enabled, - "schedule_on_time": plug.schedule_on_time, - "schedule_off_time": plug.schedule_off_time, - }) + backup["smart_plugs"].append( + { + "name": plug.name, + "ip_address": plug.ip_address, + "printer_id": plug.printer_id, + "enabled": plug.enabled, + "auto_on": plug.auto_on, + "auto_off": plug.auto_off, + "off_delay_mode": plug.off_delay_mode, + "off_delay_minutes": plug.off_delay_minutes, + "off_temp_threshold": plug.off_temp_threshold, + "username": plug.username, + "password": plug.password, + "power_alert_enabled": plug.power_alert_enabled, + "power_alert_high": plug.power_alert_high, + "power_alert_low": plug.power_alert_low, + "schedule_enabled": plug.schedule_enabled, + "schedule_on_time": plug.schedule_on_time, + "schedule_off_time": plug.schedule_off_time, + } + ) backup["included"].append("smart_plugs") # External links @@ -312,21 +326,23 @@ async def export_backup( filaments = result.scalars().all() backup["filaments"] = [] for f in filaments: - backup["filaments"].append({ - "name": f.name, - "type": f.type, - "brand": f.brand, - "color": f.color, - "color_hex": f.color_hex, - "cost_per_kg": f.cost_per_kg, - "spool_weight_g": f.spool_weight_g, - "currency": f.currency, - "density": f.density, - "print_temp_min": f.print_temp_min, - "print_temp_max": f.print_temp_max, - "bed_temp_min": f.bed_temp_min, - "bed_temp_max": f.bed_temp_max, - }) + backup["filaments"].append( + { + "name": f.name, + "type": f.type, + "brand": f.brand, + "color": f.color, + "color_hex": f.color_hex, + "cost_per_kg": f.cost_per_kg, + "spool_weight_g": f.spool_weight_g, + "currency": f.currency, + "density": f.density, + "print_temp_min": f.print_temp_min, + "print_temp_max": f.print_temp_max, + "bed_temp_min": f.bed_temp_min, + "bed_temp_max": f.bed_temp_max, + } + ) backup["included"].append("filaments") # Maintenance types and records @@ -336,14 +352,16 @@ async def export_backup( types = result.scalars().all() backup["maintenance_types"] = [] for mt in types: - backup["maintenance_types"].append({ - "name": mt.name, - "description": mt.description, - "default_interval_hours": mt.default_interval_hours, - "interval_type": mt.interval_type, - "icon": mt.icon, - "is_system": mt.is_system, - }) + backup["maintenance_types"].append( + { + "name": mt.name, + "description": mt.description, + "default_interval_hours": mt.default_interval_hours, + "interval_type": mt.interval_type, + "icon": mt.icon, + "is_system": mt.is_system, + } + ) backup["included"].append("maintenance_types") # Collect files for ZIP (icons + archives) @@ -365,9 +383,17 @@ async def export_backup( backup["archives"] = [] base_dir = app_settings.base_dir + # Build project ID to name mapping for archive export + project_id_to_name: dict[int, str] = {} + if include_projects: + proj_result = await db.execute(select(Project)) + for proj in proj_result.scalars().all(): + project_id_to_name[proj.id] = proj.name + for a in archives: archive_data = { "filename": a.filename, + "project_name": project_id_to_name.get(a.project_id) if a.project_id else None, "file_size": a.file_size, "content_hash": a.content_hash, "print_name": a.print_name, @@ -432,10 +458,61 @@ async def export_backup( backup["archives"].append(archive_data) backup["included"].append("archives") + # Projects with BOM items + if include_projects: + result = await db.execute(select(Project)) + projects = result.scalars().all() + backup["projects"] = [] + + for p in projects: + # Get BOM items for this project + bom_result = await db.execute(select(ProjectBOMItem).where(ProjectBOMItem.project_id == p.id)) + bom_items = bom_result.scalars().all() + + project_data = { + "name": p.name, + "description": p.description, + "color": p.color, + "status": p.status, + "target_count": p.target_count, + "notes": p.notes, + "tags": p.tags, + "due_date": p.due_date.isoformat() if p.due_date else None, + "priority": p.priority, + "budget": p.budget, + "is_template": p.is_template, + "bom_items": [ + { + "name": item.name, + "quantity_needed": item.quantity_needed, + "quantity_acquired": item.quantity_acquired, + "unit_price": item.unit_price, + "sourcing_url": item.sourcing_url, + "stl_filename": item.stl_filename, + "remarks": item.remarks, + "sort_order": item.sort_order, + } + for item in bom_items + ], + } + + # Include attachment files for ZIP + if p.attachments: + project_data["attachments"] = p.attachments + attachments_dir = base_dir / "projects" / str(p.id) / "attachments" + for att in p.attachments: + att_path = attachments_dir / att.get("filename", "") + if att_path.exists(): + zip_path = f"projects/{p.id}/attachments/{att['filename']}" + backup_files.append((zip_path, att_path)) + + backup["projects"].append(project_data) + backup["included"].append("projects") + # If there are files to include (icons or archives), create ZIP file if backup_files: zip_buffer = io.BytesIO() - with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf: + with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf: # Add backup.json zf.writestr("backup.json", json.dumps(backup, indent=2)) @@ -454,7 +531,7 @@ async def export_backup( return StreamingResponse( zip_buffer, media_type="application/zip", - headers={"Content-Disposition": f"attachment; filename={filename}"} + headers={"Content-Disposition": f"attachment; filename={filename}"}, ) # Otherwise return JSON @@ -462,7 +539,7 @@ async def export_backup( content=backup, headers={ "Content-Disposition": f"attachment; filename=bambuddy-backup-{datetime.now().strftime('%Y%m%d-%H%M%S')}.json" - } + }, ) @@ -479,27 +556,27 @@ async def import_backup( files_restored = 0 # Check if it's a ZIP file - if file.filename and file.filename.endswith('.zip'): + if file.filename and file.filename.endswith(".zip"): try: zip_buffer = io.BytesIO(content) - with zipfile.ZipFile(zip_buffer, 'r') as zf: + with zipfile.ZipFile(zip_buffer, "r") as zf: # Extract backup.json - if 'backup.json' not in zf.namelist(): + if "backup.json" not in zf.namelist(): return {"success": False, "message": "Invalid ZIP: missing backup.json"} - backup_content = zf.read('backup.json') + backup_content = zf.read("backup.json") backup = json.loads(backup_content.decode("utf-8")) # Extract all other files to base_dir for zip_path in zf.namelist(): - if zip_path == 'backup.json': + if zip_path == "backup.json": continue # Ensure path is safe (no path traversal) - if '..' in zip_path or zip_path.startswith('/'): + if ".." in zip_path or zip_path.startswith("/"): continue target_path = base_dir / zip_path target_path.parent.mkdir(parents=True, exist_ok=True) - with zf.open(zip_path) as src, open(target_path, 'wb') as dst: + with zf.open(zip_path) as src, open(target_path, "wb") as dst: dst.write(src.read()) files_restored += 1 except zipfile.BadZipFile: @@ -520,6 +597,7 @@ async def import_backup( "printers": 0, "filaments": 0, "maintenance_types": 0, + "projects": 0, } skipped = { "settings": 0, @@ -531,6 +609,7 @@ async def import_backup( "filaments": 0, "maintenance_types": 0, "archives": 0, + "projects": 0, } skipped_details = { "notification_providers": [], @@ -540,6 +619,7 @@ async def import_backup( "filaments": [], "maintenance_types": [], "archives": [], + "projects": [], } # Restore settings (always overwrites) @@ -616,9 +696,7 @@ async def import_backup( if "notification_templates" in backup: for template_data in backup["notification_templates"]: result = await db.execute( - select(NotificationTemplate).where( - NotificationTemplate.event_type == template_data["event_type"] - ) + select(NotificationTemplate).where(NotificationTemplate.event_type == template_data["event_type"]) ) existing = result.scalar_one_or_none() if existing: @@ -641,9 +719,7 @@ async def import_backup( # Restore smart plugs (skip or overwrite duplicates by IP) if "smart_plugs" in backup: for plug_data in backup["smart_plugs"]: - result = await db.execute( - select(SmartPlug).where(SmartPlug.ip_address == plug_data["ip_address"]) - ) + result = await db.execute(select(SmartPlug).where(SmartPlug.ip_address == plug_data["ip_address"])) existing = result.scalar_one_or_none() if existing: if overwrite: @@ -697,10 +773,7 @@ async def import_backup( for link_data in backup["external_links"]: result = await db.execute( - select(ExternalLink).where( - ExternalLink.name == link_data["name"], - ExternalLink.url == link_data["url"] - ) + select(ExternalLink).where(ExternalLink.name == link_data["name"], ExternalLink.url == link_data["url"]) ) existing = result.scalar_one_or_none() if existing: @@ -728,9 +801,7 @@ async def import_backup( # Restore printers (skip or overwrite duplicates by serial_number) if "printers" in backup: for printer_data in backup["printers"]: - result = await db.execute( - select(Printer).where(Printer.serial_number == printer_data["serial_number"]) - ) + result = await db.execute(select(Printer).where(Printer.serial_number == printer_data["serial_number"])) existing = result.scalar_one_or_none() if existing: if overwrite: @@ -805,7 +876,9 @@ async def import_backup( restored["filaments"] += 1 else: skipped["filaments"] += 1 - skipped_details["filaments"].append(f"{filament_data.get('brand', '')} {filament_data['name']} ({filament_data['type']})") + skipped_details["filaments"].append( + f"{filament_data.get('brand', '')} {filament_data['name']} ({filament_data['type']})" + ) else: filament = Filament( name=filament_data["name"], @@ -828,9 +901,7 @@ async def import_backup( # Restore maintenance types (skip or overwrite duplicates by name) if "maintenance_types" in backup: for mt_data in backup["maintenance_types"]: - result = await db.execute( - select(MaintenanceType).where(MaintenanceType.name == mt_data["name"]) - ) + result = await db.execute(select(MaintenanceType).where(MaintenanceType.name == mt_data["name"])) existing = result.scalar_one_or_none() if existing: if overwrite: @@ -861,9 +932,7 @@ async def import_backup( # Skip if no content_hash or already exists content_hash = archive_data.get("content_hash") if content_hash: - result = await db.execute( - select(PrintArchive).where(PrintArchive.content_hash == content_hash) - ) + result = await db.execute(select(PrintArchive).where(PrintArchive.content_hash == content_hash)) existing = result.scalar_one_or_none() if existing: skipped["archives"] += 1 @@ -907,15 +976,111 @@ async def import_backup( db.add(archive) restored["archives"] = restored.get("archives", 0) + 1 + # Restore projects (skip or overwrite duplicates by name) + if "projects" in backup: + for project_data in backup["projects"]: + result = await db.execute(select(Project).where(Project.name == project_data["name"])) + existing = result.scalar_one_or_none() + if existing: + if overwrite: + # Update existing project + existing.description = project_data.get("description") + existing.color = project_data.get("color") + existing.status = project_data.get("status", "active") + existing.target_count = project_data.get("target_count") + existing.notes = project_data.get("notes") + existing.tags = project_data.get("tags") + existing.priority = project_data.get("priority", "normal") + existing.budget = project_data.get("budget") + existing.is_template = project_data.get("is_template", False) + existing.attachments = project_data.get("attachments") + if project_data.get("due_date"): + existing.due_date = datetime.fromisoformat(project_data["due_date"]) + + # Delete existing BOM items and re-add + await db.execute(ProjectBOMItem.__table__.delete().where(ProjectBOMItem.project_id == existing.id)) + for bom_data in project_data.get("bom_items", []): + bom_item = ProjectBOMItem( + project_id=existing.id, + name=bom_data["name"], + quantity_needed=bom_data.get("quantity_needed", 1), + quantity_acquired=bom_data.get("quantity_acquired", 0), + unit_price=bom_data.get("unit_price"), + sourcing_url=bom_data.get("sourcing_url"), + stl_filename=bom_data.get("stl_filename"), + remarks=bom_data.get("remarks"), + sort_order=bom_data.get("sort_order", 0), + ) + db.add(bom_item) + + restored["projects"] += 1 + else: + skipped["projects"] += 1 + skipped_details["projects"].append(project_data["name"]) + else: + # Create new project + project = Project( + name=project_data["name"], + description=project_data.get("description"), + color=project_data.get("color"), + status=project_data.get("status", "active"), + target_count=project_data.get("target_count"), + notes=project_data.get("notes"), + tags=project_data.get("tags"), + priority=project_data.get("priority", "normal"), + budget=project_data.get("budget"), + is_template=project_data.get("is_template", False), + attachments=project_data.get("attachments"), + ) + if project_data.get("due_date"): + project.due_date = datetime.fromisoformat(project_data["due_date"]) + + db.add(project) + await db.flush() # Get the project ID + + # Add BOM items + for bom_data in project_data.get("bom_items", []): + bom_item = ProjectBOMItem( + project_id=project.id, + name=bom_data["name"], + quantity_needed=bom_data.get("quantity_needed", 1), + quantity_acquired=bom_data.get("quantity_acquired", 0), + unit_price=bom_data.get("unit_price"), + sourcing_url=bom_data.get("sourcing_url"), + stl_filename=bom_data.get("stl_filename"), + remarks=bom_data.get("remarks"), + sort_order=bom_data.get("sort_order", 0), + ) + db.add(bom_item) + + restored["projects"] += 1 + + # Link archives to projects by name (after both are restored) + if "archives" in backup and "projects" in backup: + # Build project name to ID mapping + proj_result = await db.execute(select(Project)) + project_name_to_id: dict[str, int] = {} + for proj in proj_result.scalars().all(): + project_name_to_id[proj.name] = proj.id + + # Update archives with project_id + for archive_data in backup["archives"]: + project_name = archive_data.get("project_name") + if project_name and project_name in project_name_to_id: + content_hash = archive_data.get("content_hash") + if content_hash: + result = await db.execute(select(PrintArchive).where(PrintArchive.content_hash == content_hash)) + archive = result.scalar_one_or_none() + if archive: + archive.project_id = project_name_to_id[project_name] + await db.commit() # If printers were in the backup (restored, updated, or skipped), reconnect all active printers # This ensures connections are re-established after restore, even if printers were skipped if "printers" in backup: # Need fresh query after commit to get proper IDs for newly created printers - result = await db.execute( - select(Printer).where(Printer.is_active == True) - ) + result = await db.execute(select(Printer).where(Printer.is_active.is_(True))) active_printers = result.scalars().all() for printer in active_printers: # This will disconnect existing connection (if any) and reconnect diff --git a/backend/app/core/database.py b/backend/app/core/database.py index 63a5b6054..d1a3c74a0 100644 --- a/backend/app/core/database.py +++ b/backend/app/core/database.py @@ -335,6 +335,30 @@ async def run_migrations(conn): except Exception: pass + # Migration: Rename quantity_printed to quantity_acquired in project_bom_items + try: + await conn.execute(text("ALTER TABLE project_bom_items RENAME COLUMN quantity_printed TO quantity_acquired")) + except Exception: + pass + + # Migration: Add unit_price column to project_bom_items + try: + await conn.execute(text("ALTER TABLE project_bom_items ADD COLUMN unit_price REAL")) + except Exception: + pass + + # Migration: Add sourcing_url column to project_bom_items + try: + await conn.execute(text("ALTER TABLE project_bom_items ADD COLUMN sourcing_url VARCHAR(512)")) + except Exception: + pass + + # Migration: Rename notes to remarks in project_bom_items + try: + await conn.execute(text("ALTER TABLE project_bom_items RENAME COLUMN notes TO remarks")) + except Exception: + pass + async def seed_notification_templates(): """Seed default notification templates if they don't exist.""" diff --git a/backend/app/models/project_bom.py b/backend/app/models/project_bom.py index 89fdd58c5..c3fb8a879 100644 --- a/backend/app/models/project_bom.py +++ b/backend/app/models/project_bom.py @@ -1,13 +1,17 @@ from datetime import datetime -from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, func +from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, Text, func from sqlalchemy.orm import Mapped, mapped_column, relationship from backend.app.core.database import Base class ProjectBOMItem(Base): - """Bill of Materials item for a project.""" + """Bill of Materials item for a project. + + Tracks sourced/purchased parts (hardware, electronics, screws, etc.) + that need to be acquired for a project. + """ __tablename__ = "project_bom_items" @@ -15,16 +19,20 @@ class ProjectBOMItem(Base): project_id: Mapped[int] = mapped_column(ForeignKey("projects.id", ondelete="CASCADE")) name: Mapped[str] = mapped_column(String(255)) quantity_needed: Mapped[int] = mapped_column(Integer, default=1) - quantity_printed: Mapped[int] = mapped_column(Integer, default=0) + quantity_acquired: Mapped[int] = mapped_column(Integer, default=0) - # Optional link to archive that prints this part + # Sourcing information + unit_price: Mapped[float | None] = mapped_column(Float, nullable=True) + sourcing_url: Mapped[str | None] = mapped_column(String(512), nullable=True) + + # Optional link to archive (for reference) archive_id: Mapped[int | None] = mapped_column(ForeignKey("print_archives.id", ondelete="SET NULL"), nullable=True) - # Reference to attachment filename (STL file) + # Reference to attachment filename stl_filename: Mapped[str | None] = mapped_column(String(255), nullable=True) - # Notes about this part - notes: Mapped[str | None] = mapped_column(Text, nullable=True) + # Remarks about this part + remarks: Mapped[str | None] = mapped_column(Text, nullable=True) # Sort order sort_order: Mapped[int] = mapped_column(Integer, default=0) diff --git a/backend/app/schemas/project.py b/backend/app/schemas/project.py index a7b25be04..2fa8a5c14 100644 --- a/backend/app/schemas/project.py +++ b/backend/app/schemas/project.py @@ -100,6 +100,8 @@ class ArchivePreview(BaseModel): print_name: str | None thumbnail_path: str | None status: str + filament_type: str | None = None + filament_color: str | None = None class ProjectListResponse(BaseModel): @@ -135,15 +137,17 @@ class BatchAddQueueItems(BaseModel): queue_item_ids: list[int] -# Phase 7: BOM Schemas +# Phase 7: BOM Schemas - Tracks sourced/purchased parts class BOMItemCreate(BaseModel): """Schema for creating a BOM item.""" name: str quantity_needed: int = 1 + unit_price: float | None = None + sourcing_url: str | None = None archive_id: int | None = None stl_filename: str | None = None - notes: str | None = None + remarks: str | None = None class BOMItemUpdate(BaseModel): @@ -151,10 +155,12 @@ class BOMItemUpdate(BaseModel): name: str | None = None quantity_needed: int | None = None - quantity_printed: int | None = None + quantity_acquired: int | None = None + unit_price: float | None = None + sourcing_url: str | None = None archive_id: int | None = None stl_filename: str | None = None - notes: str | None = None + remarks: str | None = None class BOMItemResponse(BaseModel): @@ -164,11 +170,13 @@ class BOMItemResponse(BaseModel): project_id: int name: str quantity_needed: int - quantity_printed: int + quantity_acquired: int + unit_price: float | None + sourcing_url: str | None archive_id: int | None archive_name: str | None = None stl_filename: str | None - notes: str | None + remarks: str | None sort_order: int is_complete: bool = False created_at: datetime diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index dd74db6ec..66d4dda06 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -384,6 +384,8 @@ export interface ArchivePreview { print_name: string | null; thumbnail_path: string | null; status: string; + filament_type: string | null; + filament_color: string | null; } export interface ProjectListItem { @@ -427,17 +429,19 @@ export interface ProjectUpdate { parent_id?: number; } -// BOM Types +// BOM Types - Tracks sourced/purchased parts (hardware, electronics, etc.) export interface BOMItem { id: number; project_id: number; name: string; quantity_needed: number; - quantity_printed: number; + quantity_acquired: number; + unit_price: number | null; + sourcing_url: string | null; archive_id: number | null; archive_name: string | null; stl_filename: string | null; - notes: string | null; + remarks: string | null; sort_order: number; is_complete: boolean; created_at: string; @@ -447,18 +451,22 @@ export interface BOMItem { export interface BOMItemCreate { name: string; quantity_needed?: number; + unit_price?: number; + sourcing_url?: string; archive_id?: number; stl_filename?: string; - notes?: string; + remarks?: string; } export interface BOMItemUpdate { name?: string; quantity_needed?: number; - quantity_printed?: number; + quantity_acquired?: number; + unit_price?: number; + sourcing_url?: string; archive_id?: number; stl_filename?: string; - notes?: string; + remarks?: string; } // Timeline Types @@ -1572,10 +1580,12 @@ export const api = { if (categories.notifications !== undefined) params.set('include_notifications', String(categories.notifications)); if (categories.templates !== undefined) params.set('include_templates', String(categories.templates)); if (categories.smart_plugs !== undefined) params.set('include_smart_plugs', String(categories.smart_plugs)); + if (categories.external_links !== undefined) params.set('include_external_links', String(categories.external_links)); if (categories.printers !== undefined) params.set('include_printers', String(categories.printers)); if (categories.filaments !== undefined) params.set('include_filaments', String(categories.filaments)); if (categories.maintenance !== undefined) params.set('include_maintenance', String(categories.maintenance)); if (categories.archives !== undefined) params.set('include_archives', String(categories.archives)); + if (categories.projects !== undefined) params.set('include_projects', String(categories.projects)); if (categories.access_codes !== undefined) params.set('include_access_codes', String(categories.access_codes)); } const url = `${API_BASE}/settings/backup${params.toString() ? '?' + params.toString() : ''}`; diff --git a/frontend/src/components/BackupModal.tsx b/frontend/src/components/BackupModal.tsx index 20d8ec4fe..7e6af1bd7 100644 --- a/frontend/src/components/BackupModal.tsx +++ b/frontend/src/components/BackupModal.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Download, X, Settings, Bell, FileText, Plug, Printer, Palette, Wrench, Archive, Loader2, Key, AlertTriangle } from 'lucide-react'; +import { Download, X, Settings, Bell, FileText, Plug, Printer, Palette, Wrench, Archive, Loader2, Key, AlertTriangle, Link, FolderKanban } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Card, CardContent } from './Card'; import { Button } from './Button'; @@ -47,6 +47,14 @@ const BACKUP_CATEGORIES: BackupCategory[] = [ default: true, description: 'Tasmota plug configurations', }, + { + id: 'external_links', + labelKey: 'backup.categories.externalLinks', + defaultLabel: 'External Links', + icon: , + default: true, + description: 'Sidebar links to external services', + }, { id: 'printers', labelKey: 'backup.categories.printers', @@ -79,6 +87,14 @@ const BACKUP_CATEGORIES: BackupCategory[] = [ default: false, description: 'All print data + files (3MF, thumbnails, photos)', }, + { + id: 'projects', + labelKey: 'backup.categories.projects', + defaultLabel: 'Projects', + icon: , + default: false, + description: 'Projects, BOM items, and attachments', + }, ]; interface BackupModalProps { diff --git a/frontend/src/pages/ProjectDetailPage.tsx b/frontend/src/pages/ProjectDetailPage.tsx index d9b578fa6..3a2d1468c 100644 --- a/frontend/src/pages/ProjectDetailPage.tsx +++ b/frontend/src/pages/ProjectDetailPage.tsx @@ -23,13 +23,13 @@ import { Download, Trash2, File, - DollarSign, - ClipboardList, Plus, History, FolderTree, Copy, Layers, + ExternalLink, + ShoppingCart, } from 'lucide-react'; import { api } from '../api/client'; import type { Archive, ProjectUpdate, BOMItem, BOMItemCreate } from '../api/client'; @@ -37,6 +37,7 @@ import { Card, CardContent } from '../components/Card'; import { Button } from '../components/Button'; import { useToast } from '../contexts/ToastContext'; import { RichTextEditor } from '../components/RichTextEditor'; +import { ConfirmModal } from '../components/ConfirmModal'; // Project edit modal (reused from ProjectsPage) import { ProjectModal } from './ProjectsPage'; @@ -227,6 +228,13 @@ export function ProjectDetailPage() { enabled: projectId > 0, }); + const { data: settings } = useQuery({ + queryKey: ['settings'], + queryFn: api.getSettings, + }); + + const currency = settings?.currency || '$'; + const updateMutation = useMutation({ mutationFn: (data: ProjectUpdate) => api.updateProject(projectId, data), onSuccess: () => { @@ -261,9 +269,9 @@ export function ProjectDetailPage() { setUploadingAttachment(true); try { - await api.uploadProjectAttachment(projectId, file); + const result = await api.uploadProjectAttachment(projectId, file); queryClient.invalidateQueries({ queryKey: ['project', projectId] }); - showToast('Attachment uploaded', 'success'); + showToast(`Uploaded: ${result.original_name}`, 'success'); } catch (error) { showToast((error as Error).message, 'error'); } finally { @@ -274,16 +282,22 @@ export function ProjectDetailPage() { } }; - const handleDeleteAttachment = async (filename: string) => { - if (!confirm('Delete this attachment?')) return; - - try { - await api.deleteProjectAttachment(projectId, filename); - queryClient.invalidateQueries({ queryKey: ['project', projectId] }); - showToast('Attachment deleted', 'success'); - } catch (error) { - showToast((error as Error).message, 'error'); - } + const handleDeleteAttachment = (filename: string, originalName: string) => { + setConfirmModal({ + isOpen: true, + title: 'Delete Attachment', + message: `Are you sure you want to delete "${originalName}"?`, + onConfirm: async () => { + setConfirmModal(prev => ({ ...prev, isOpen: false })); + try { + await api.deleteProjectAttachment(projectId, filename); + queryClient.invalidateQueries({ queryKey: ['project', projectId] }); + showToast('Attachment deleted', 'success'); + } catch (error) { + showToast((error as Error).message, 'error'); + } + }, + }); }; const formatFileSize = (bytes: number): string => { @@ -295,6 +309,19 @@ export function ProjectDetailPage() { // BOM handlers const [newBomName, setNewBomName] = useState(''); const [newBomQty, setNewBomQty] = useState(1); + const [newBomPrice, setNewBomPrice] = useState(''); + const [newBomUrl, setNewBomUrl] = useState(''); + const [newBomRemarks, setNewBomRemarks] = useState(''); + const [showBomForm, setShowBomForm] = useState(false); + const [hideBomCompleted, setHideBomCompleted] = useState(false); + + // Confirm modal state + const [confirmModal, setConfirmModal] = useState<{ + isOpen: boolean; + title: string; + message: string; + onConfirm: () => void; + }>({ isOpen: false, title: '', message: '', onConfirm: () => {} }); const createBomMutation = useMutation({ mutationFn: (data: BOMItemCreate) => api.createBOMItem(projectId, data), @@ -303,13 +330,17 @@ export function ProjectDetailPage() { queryClient.invalidateQueries({ queryKey: ['project', projectId] }); setNewBomName(''); setNewBomQty(1); - showToast('BOM item added', 'success'); + setNewBomPrice(''); + setNewBomUrl(''); + setNewBomRemarks(''); + setShowBomForm(false); + showToast('Part added', 'success'); }, onError: (error: Error) => showToast(error.message, 'error'), }); const updateBomMutation = useMutation({ - mutationFn: ({ itemId, data }: { itemId: number; data: { quantity_printed: number } }) => + mutationFn: ({ itemId, data }: { itemId: number; data: { quantity_acquired?: number } }) => api.updateBOMItem(projectId, itemId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['project-bom', projectId] }); @@ -323,7 +354,7 @@ export function ProjectDetailPage() { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['project-bom', projectId] }); queryClient.invalidateQueries({ queryKey: ['project', projectId] }); - showToast('BOM item deleted', 'success'); + showToast('Part removed', 'success'); }, onError: (error: Error) => showToast(error.message, 'error'), }); @@ -331,20 +362,33 @@ export function ProjectDetailPage() { const handleAddBomItem = (e: React.FormEvent) => { e.preventDefault(); if (!newBomName.trim()) return; - createBomMutation.mutate({ name: newBomName.trim(), quantity_needed: newBomQty }); - }; - - const handleIncrementPrinted = (item: BOMItem) => { - updateBomMutation.mutate({ - itemId: item.id, - data: { quantity_printed: item.quantity_printed + 1 }, + createBomMutation.mutate({ + name: newBomName.trim(), + quantity_needed: newBomQty, + unit_price: newBomPrice ? parseFloat(newBomPrice) : undefined, + sourcing_url: newBomUrl.trim() || undefined, + remarks: newBomRemarks.trim() || undefined, }); }; - const handleDeleteBomItem = (itemId: number) => { - if (confirm('Delete this BOM item?')) { - deleteBomMutation.mutate(itemId); - } + const handleToggleAcquired = (item: BOMItem) => { + const newQty = item.is_complete ? 0 : item.quantity_needed; + updateBomMutation.mutate({ + itemId: item.id, + data: { quantity_acquired: newQty }, + }); + }; + + const handleDeleteBomItem = (itemId: number, itemName: string) => { + setConfirmModal({ + isOpen: true, + title: 'Delete Part', + message: `Are you sure you want to delete "${itemName}"?`, + onConfirm: () => { + setConfirmModal(prev => ({ ...prev, isOpen: false })); + deleteBomMutation.mutate(itemId); + }, + }); }; // Template handlers @@ -395,7 +439,7 @@ export function ProjectDetailPage() { : null; return ( -
+
{/* Breadcrumb */}
@@ -503,15 +547,14 @@ export function ProjectDetailPage() { {stats && (stats.estimated_cost > 0 || project.budget) && ( -

- +

Cost Tracking

Filament Cost

- ${stats.estimated_cost.toFixed(2)} + {currency}{stats.estimated_cost.toFixed(2)}

{stats.total_energy_kwh > 0 && ( @@ -521,7 +564,7 @@ export function ProjectDetailPage() { {stats.total_energy_kwh.toFixed(2)} kWh {stats.total_energy_cost > 0 && ( - (${stats.total_energy_cost.toFixed(2)}) + ({currency}{stats.total_energy_cost.toFixed(2)}) )}

@@ -531,12 +574,12 @@ export function ProjectDetailPage() { <>

Budget

-

${project.budget.toFixed(2)}

+

{currency}{project.budget.toFixed(2)}

Remaining

= 0 ? 'text-bambu-green' : 'text-red-400'}`}> - ${(project.budget - stats.estimated_cost).toFixed(2)} + {currency}{(project.budget - stats.estimated_cost).toFixed(2)}

@@ -734,6 +777,10 @@ export function ProjectDetailPage() {

+

+ Upload any file: images (PNG, JPG), PDFs, STL files, or documents. +

+ {project.attachments && project.attachments.length > 0 ? (
{project.attachments.map((attachment) => ( @@ -762,7 +809,7 @@ export function ProjectDetailPage() {
) : (

- No attachments. Upload STL files, reference images, or other documents. + No attachments yet. Click Upload to add files.

)} - {/* BOM Section */} + {/* BOM Section - Parts to source/purchase */} -
+

- + Bill of Materials {stats && stats.bom_total_items > 0 && ( - ({stats.bom_completed_items}/{stats.bom_total_items} complete) + ({stats.bom_completed_items}/{stats.bom_total_items} acquired) )}

+
+ {bomItems && bomItems.some(item => item.is_complete) && ( + + )} + {!showBomForm && ( + + )} +
{/* Add BOM item form */} -
- setNewBomName(e.target.value)} - className="flex-1 bg-bambu-dark border border-bambu-dark-tertiary rounded px-3 py-2 text-sm text-white placeholder-bambu-gray focus:outline-none focus:border-bambu-green" - placeholder="Part name..." - /> - setNewBomQty(parseInt(e.target.value) || 1)} - className="w-20 bg-bambu-dark border border-bambu-dark-tertiary rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-bambu-green" - min="1" - /> - -
+ {showBomForm && ( +
+
+ setNewBomName(e.target.value)} + className="bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded px-3 py-2 text-sm text-white placeholder-bambu-gray focus:outline-none focus:border-bambu-green" + placeholder="Part name (e.g., M3x8 screws)" + autoFocus + /> +
+ setNewBomQty(parseInt(e.target.value) || 1)} + className="w-20 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-bambu-green" + min="1" + placeholder="Qty" + /> + setNewBomPrice(e.target.value)} + className="flex-1 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded px-3 py-2 text-sm text-white placeholder-bambu-gray focus:outline-none focus:border-bambu-green" + placeholder={`Price (${currency})`} + /> +
+
+ setNewBomUrl(e.target.value)} + className="w-full bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded px-3 py-2 text-sm text-white placeholder-bambu-gray focus:outline-none focus:border-bambu-green" + placeholder="Sourcing URL (optional)" + /> + setNewBomRemarks(e.target.value)} + className="w-full bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded px-3 py-2 text-sm text-white placeholder-bambu-gray focus:outline-none focus:border-bambu-green" + placeholder="Remarks (optional)" + /> +
+ + +
+
+ )} {bomLoading ? (
@@ -826,55 +928,94 @@ export function ProjectDetailPage() {
) : bomItems && bomItems.length > 0 ? (
- {bomItems.map((item) => ( + {bomItems + .filter(item => !hideBomCompleted || !item.is_complete) + .map((item) => (
-
+
-
-

- {item.name} -

-

- {item.quantity_printed} / {item.quantity_needed} printed -

+
+
+
+

+ {item.name} + + x{item.quantity_needed} + +

+ {item.unit_price !== null && ( + + {currency}{(item.unit_price * item.quantity_needed).toFixed(2)} + + )} +
+ +
+ {/* Sourcing URL */} + {item.sourcing_url && ( + e.stopPropagation()} + > + + + {(() => { + try { + return new URL(item.sourcing_url).hostname.replace('www.', ''); + } catch { + return item.sourcing_url; + } + })()} + + + )} + {/* Remarks */} + {item.remarks && ( +

+ {item.remarks} +

+ )}
-
- - -
))} + {/* BOM Total */} + {bomItems.some(item => item.unit_price !== null) && ( +
+ Total cost: + + {currency}{bomItems.reduce((sum, item) => sum + (item.unit_price || 0) * item.quantity_needed, 0).toFixed(2)} + +
+ )}
) : (

- No parts in the bill of materials. Add parts to track what needs to be printed. + No parts in the bill of materials. Add hardware, electronics, or other components to track what needs to be sourced.

)} @@ -1011,6 +1152,18 @@ export function ProjectDetailPage() { isLoading={updateMutation.isPending} /> )} + + {/* Confirm Modal */} + {confirmModal.isOpen && ( + setConfirmModal(prev => ({ ...prev, isOpen: false }))} + /> + )}
); } diff --git a/frontend/src/pages/ProjectsPage.tsx b/frontend/src/pages/ProjectsPage.tsx index 4f487d2bc..77b524bc6 100644 --- a/frontend/src/pages/ProjectsPage.tsx +++ b/frontend/src/pages/ProjectsPage.tsx @@ -240,13 +240,16 @@ function ProjectCard({ project, onClick, onEdit, onDelete }: ProjectCardProps) { return (
- {/* Color accent bar */} + {/* Color accent bar with glow */}
@@ -257,8 +260,21 @@ function ProjectCard({ project, onClick, onEdit, onDelete }: ProjectCardProps) {
-
+

{project.name}

+ {project.target_count ? ( + = 100 + ? 'bg-bambu-green/20 text-bambu-green' + : 'bg-bambu-dark text-bambu-gray' + }`}> + {project.archive_count}/{project.target_count} parts + + ) : project.archive_count > 0 ? ( + + {project.archive_count} print{project.archive_count !== 1 ? 's' : ''} + + ) : null} {isCompleted && ( Done @@ -275,6 +291,38 @@ function ProjectCard({ project, onClick, onEdit, onDelete }: ProjectCardProps) { {project.description}

)} + {/* Filament materials/colors */} + {project.archives && project.archives.length > 0 && (() => { + const materials = [...new Set(project.archives.map(a => a.filament_type).filter(Boolean))]; + const colors = [...new Set(project.archives.map(a => a.filament_color).filter(Boolean))] as string[]; + if (materials.length === 0 && colors.length === 0) return null; + return ( +
+ {/* Material types as text badges */} + {materials.slice(0, 3).map((mat) => ( + + {mat} + + ))} + {/* Colors as swatches */} + {colors.length > 0 && ( +
+ {colors.slice(0, 5).map((col) => ( +
+ ))} + {colors.length > 5 && ( + +{colors.length - 5} + )} +
+ )} +
+ ); + })()}
@@ -310,38 +358,60 @@ function ProjectCard({ project, onClick, onEdit, onDelete }: ProjectCardProps) {
- {/* Progress section */} - {project.target_count ? ( -
-
- Progress - = 100 ? 'text-bambu-green font-medium' : 'text-white'}> - {project.archive_count} / {project.target_count} - + {/* Progress section - show for all projects */} +
+ {project.target_count ? ( + <> +
+ Progress + = 100 ? 'text-bambu-green font-medium' : 'text-white'}> + {project.archive_count} / {project.target_count} + +
+
+
= 100 + ? 'linear-gradient(90deg, #22c55e, #4ade80)' + : `linear-gradient(90deg, ${project.color || '#6b7280'}, ${project.color || '#6b7280'}cc)`, + boxShadow: `0 0 8px ${progressPercent >= 100 ? '#22c55e' : project.color || '#6b7280'}60` + }} + /> +
+
+ {progressPercent.toFixed(0)}% complete +
+ + ) : project.archive_count > 0 ? ( +
+
+ + {project.archive_count} print{project.archive_count !== 1 ? 's' : ''} completed +
+ {project.queue_count > 0 && ( +
+ + {project.queue_count} in queue +
+ )}
-
-
= 100 ? '#22c55e' : project.color || '#6b7280', - }} - /> + ) : ( +
+ No prints yet
-
- {progressPercent.toFixed(0)}% complete -
-
- ) : null} + )} +
- {/* Archive thumbnails grid */} + {/* Archive thumbnails - compact 4-column grid */} {project.archives && project.archives.length > 0 && (
-
+
{project.archives.slice(0, 4).map((archive) => (
{archive.thumbnail_path ? ( @@ -352,22 +422,22 @@ function ProjectCard({ project, onClick, onEdit, onDelete }: ProjectCardProps) { /> ) : (
- +
)} {archive.status === 'failed' && ( -
+
)}
))} - {project.archive_count > 4 && ( -
- +{project.archive_count - 4} -
- )}
+ {project.archive_count > 4 && ( +

+ +{project.archive_count - 4} more +

+ )}
)} @@ -480,17 +550,17 @@ export function ProjectsPage() { }, {} as Record) || {}; return ( -
+
{/* Header */}

-
+
Projects

-

+

Organize and track your 3D printing projects

@@ -560,7 +630,7 @@ export function ProjectsPage() { )}
) : ( -
+
{projects?.map((project) => ( r[i]})}}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const a of i)if(a.type==="childList")for(const s of a.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(i){const a={};return i.integrity&&(a.integrity=i.integrity),i.referrerPolicy&&(a.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?a.credentials="include":i.crossOrigin==="anonymous"?a.credentials="omit":a.credentials="same-origin",a}function r(i){if(i.ep)return;i.ep=!0;const a=n(i);fetch(i.href,a)}})();var pv=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function mo(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var SA={exports:{}},ox={};var lF;function Rre(){if(lF)return ox;lF=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function n(r,i,a){var s=null;if(a!==void 0&&(s=""+a),i.key!==void 0&&(s=""+i.key),"key"in i){a={};for(var o in i)o!=="key"&&(a[o]=i[o])}else a=i;return i=a.ref,{$$typeof:t,type:r,key:s,ref:i!==void 0?i:null,props:a}}return ox.Fragment=e,ox.jsx=n,ox.jsxs=n,ox}var cF;function jre(){return cF||(cF=1,SA.exports=Rre()),SA.exports}var l=jre(),MA={exports:{}},en={};var uF;function Dre(){if(uF)return en;uF=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),n=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),a=Symbol.for("react.consumer"),s=Symbol.for("react.context"),o=Symbol.for("react.forward_ref"),c=Symbol.for("react.suspense"),u=Symbol.for("react.memo"),d=Symbol.for("react.lazy"),f=Symbol.for("react.activity"),g=Symbol.iterator;function p(z){return z===null||typeof z!="object"?null:(z=g&&z[g]||z["@@iterator"],typeof z=="function"?z:null)}var x={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},v=Object.assign,y={};function b(z,$,J){this.props=z,this.context=$,this.refs=y,this.updater=J||x}b.prototype.isReactComponent={},b.prototype.setState=function(z,$){if(typeof z!="object"&&typeof z!="function"&&z!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,z,$,"setState")},b.prototype.forceUpdate=function(z){this.updater.enqueueForceUpdate(this,z,"forceUpdate")};function E(){}E.prototype=b.prototype;function w(z,$,J){this.props=z,this.context=$,this.refs=y,this.updater=J||x}var A=w.prototype=new E;A.constructor=w,v(A,b.prototype),A.isPureReactComponent=!0;var T=Array.isArray;function N(){}var C={H:null,A:null,T:null,S:null},U=Object.prototype.hasOwnProperty;function k(z,$,J){var ie=J.ref;return{$$typeof:t,type:z,key:$,ref:ie!==void 0?ie:null,props:J}}function j(z,$){return k(z.type,$,z.props)}function I(z){return typeof z=="object"&&z!==null&&z.$$typeof===t}function L(z){var $={"=":"=0",":":"=2"};return"$"+z.replace(/[=:]/g,function(J){return $[J]})}var q=/\/+/g;function R(z,$){return typeof z=="object"&&z!==null&&z.key!=null?L(""+z.key):$.toString(36)}function W(z){switch(z.status){case"fulfilled":return z.value;case"rejected":throw z.reason;default:switch(typeof z.status=="string"?z.then(N,N):(z.status="pending",z.then(function($){z.status==="pending"&&(z.status="fulfilled",z.value=$)},function($){z.status==="pending"&&(z.status="rejected",z.reason=$)})),z.status){case"fulfilled":return z.value;case"rejected":throw z.reason}}throw z}function K(z,$,J,ie,pe){var fe=typeof z;(fe==="undefined"||fe==="boolean")&&(z=null);var ye=!1;if(z===null)ye=!0;else switch(fe){case"bigint":case"string":case"number":ye=!0;break;case"object":switch(z.$$typeof){case t:case e:ye=!0;break;case d:return ye=z._init,K(ye(z._payload),$,J,ie,pe)}}if(ye)return pe=pe(z),ye=ie===""?"."+R(z,0):ie,T(pe)?(J="",ye!=null&&(J=ye.replace(q,"$&/")+"/"),K(pe,$,J,"",function(we){return we})):pe!=null&&(I(pe)&&(pe=j(pe,J+(pe.key==null||z&&z.key===pe.key?"":(""+pe.key).replace(q,"$&/")+"/")+ye)),$.push(pe)),1;ye=0;var Re=ie===""?".":ie+":";if(T(z))for(var re=0;re>>1,Q=K[Y];if(0>>1;Yi(J,ee))iei(pe,J)?(K[Y]=pe,K[ie]=ee,Y=ie):(K[Y]=J,K[$]=ee,Y=$);else if(iei(pe,ee))K[Y]=pe,K[ie]=ee,Y=ie;else break e}}return G}function i(K,G){var ee=K.sortIndex-G.sortIndex;return ee!==0?ee:K.id-G.id}if(t.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var a=performance;t.unstable_now=function(){return a.now()}}else{var s=Date,o=s.now();t.unstable_now=function(){return s.now()-o}}var c=[],u=[],d=1,f=null,g=3,p=!1,x=!1,v=!1,y=!1,b=typeof setTimeout=="function"?setTimeout:null,E=typeof clearTimeout=="function"?clearTimeout:null,w=typeof setImmediate<"u"?setImmediate:null;function A(K){for(var G=n(u);G!==null;){if(G.callback===null)r(u);else if(G.startTime<=K)r(u),G.sortIndex=G.expirationTime,e(c,G);else break;G=n(u)}}function T(K){if(v=!1,A(K),!x)if(n(c)!==null)x=!0,N||(N=!0,L());else{var G=n(u);G!==null&&W(T,G.startTime-K)}}var N=!1,C=-1,U=5,k=-1;function j(){return y?!0:!(t.unstable_now()-kK&&j());){var Y=f.callback;if(typeof Y=="function"){f.callback=null,g=f.priorityLevel;var Q=Y(f.expirationTime<=K);if(K=t.unstable_now(),typeof Q=="function"){f.callback=Q,A(K),G=!0;break t}f===n(c)&&r(c),A(K)}else r(c);f=n(c)}if(f!==null)G=!0;else{var z=n(u);z!==null&&W(T,z.startTime-K),G=!1}}break e}finally{f=null,g=ee,p=!1}G=void 0}}finally{G?L():N=!1}}}var L;if(typeof w=="function")L=function(){w(I)};else if(typeof MessageChannel<"u"){var q=new MessageChannel,R=q.port2;q.port1.onmessage=I,L=function(){R.postMessage(null)}}else L=function(){b(I,0)};function W(K,G){C=b(function(){K(t.unstable_now())},G)}t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(K){K.callback=null},t.unstable_forceFrameRate=function(K){0>K||125Y?(K.sortIndex=ee,e(u,K),n(c)===null&&K===n(u)&&(v?(E(C),C=-1):v=!0,W(T,ee-Y))):(K.sortIndex=Q,e(c,K),x||p||(x=!0,N||(N=!0,L()))),K},t.unstable_shouldYield=j,t.unstable_wrapCallback=function(K){var G=g;return function(){var ee=g;g=G;try{return K.apply(this,arguments)}finally{g=ee}}}})(TA)),TA}var fF;function Ire(){return fF||(fF=1,AA.exports=Lre()),AA.exports}var CA={exports:{}},ia={};var mF;function Fre(){if(mF)return ia;mF=1;var t=Kp();function e(c){var u="https://react.dev/errors/"+c;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(t)}catch(e){console.error(e)}}return t(),CA.exports=Fre(),CA.exports}var gF;function Ure(){if(gF)return lx;gF=1;var t=Ire(),e=Kp(),n=C9();function r(h){var m="https://react.dev/errors/"+h;if(1Q||(h.current=Y[Q],Y[Q]=null,Q--)}function J(h,m){Q++,Y[Q]=h.current,h.current=m}var ie=z(null),pe=z(null),fe=z(null),ye=z(null);function Re(h,m){switch(J(fe,m),J(pe,h),J(ie,null),m.nodeType){case 9:case 11:h=(h=m.documentElement)&&(h=h.namespaceURI)?P5(h):0;break;default:if(h=m.tagName,m=m.namespaceURI)m=P5(m),h=R5(m,h);else switch(h){case"svg":h=1;break;case"math":h=2;break;default:h=0}}$(ie),J(ie,h)}function re(){$(ie),$(pe),$(fe)}function we(h){h.memoizedState!==null&&J(ye,h);var m=ie.current,_=R5(m,h.type);m!==_&&(J(pe,h),J(ie,_))}function Oe(h){pe.current===h&&($(ie),$(pe)),ye.current===h&&($(ye),rx._currentValue=ee)}var ce,Ie;function B(h){if(ce===void 0)try{throw Error()}catch(_){var m=_.stack.trim().match(/\n( *(at )?)/);ce=m&&m[1]||"",Ie=-1<_.stack.indexOf(` + at`)?" ()":-1<_.stack.indexOf("@")?"@unknown:0:0":""}return` +`+ce+h+Ie}var H=!1;function ne(h,m){if(!h||H)return"";H=!0;var _=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var S={DetermineComponentFrameRoot:function(){try{if(m){var ht=function(){throw Error()};if(Object.defineProperty(ht.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(ht,[])}catch(st){var Je=st}Reflect.construct(h,[],ht)}else{try{ht.call()}catch(st){Je=st}h.call(ht.prototype)}}else{try{throw Error()}catch(st){Je=st}(ht=h())&&typeof ht.catch=="function"&&ht.catch(function(){})}}catch(st){if(st&&Je&&typeof st.stack=="string")return[st.stack,Je.stack]}return[null,null]}};S.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var P=Object.getOwnPropertyDescriptor(S.DetermineComponentFrameRoot,"name");P&&P.configurable&&Object.defineProperty(S.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var D=S.DetermineComponentFrameRoot(),X=D[0],le=D[1];if(X&&le){var Te=X.split(` +`),Ze=le.split(` +`);for(P=S=0;SP||Te[S]!==Ze[P]){var ct=` +`+Te[S].replace(" at new "," at ");return h.displayName&&ct.includes("")&&(ct=ct.replace("",h.displayName)),ct}while(1<=S&&0<=P);break}}}finally{H=!1,Error.prepareStackTrace=_}return(_=h?h.displayName||h.name:"")?B(_):""}function V(h,m){switch(h.tag){case 26:case 27:case 5:return B(h.type);case 16:return B("Lazy");case 13:return h.child!==m&&m!==null?B("Suspense Fallback"):B("Suspense");case 19:return B("SuspenseList");case 0:case 15:return ne(h.type,!1);case 11:return ne(h.type.render,!1);case 1:return ne(h.type,!0);case 31:return B("Activity");default:return""}}function Z(h){try{var m="",_=null;do m+=V(h,_),_=h,h=h.return;while(h);return m}catch(S){return` +Error generating stack: `+S.message+` +`+S.stack}}var se=Object.prototype.hasOwnProperty,xe=t.unstable_scheduleCallback,Ee=t.unstable_cancelCallback,Me=t.unstable_shouldYield,F=t.unstable_requestPaint,O=t.unstable_now,te=t.unstable_getCurrentPriorityLevel,ve=t.unstable_ImmediatePriority,Se=t.unstable_UserBlockingPriority,be=t.unstable_NormalPriority,rt=t.unstable_LowPriority,He=t.unstable_IdlePriority,de=t.log,We=t.unstable_setDisableYieldValue,Ue=null,oe=null;function tt(h){if(typeof de=="function"&&We(h),oe&&typeof oe.setStrictMode=="function")try{oe.setStrictMode(Ue,h)}catch{}}var $e=Math.clz32?Math.clz32:he,Ye=Math.log,ut=Math.LN2;function he(h){return h>>>=0,h===0?32:31-(Ye(h)/ut|0)|0}var Qe=256,ge=262144,ze=4194304;function ke(h){var m=h&42;if(m!==0)return m;switch(h&-h){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return h&261888;case 262144:case 524288:case 1048576:case 2097152:return h&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return h&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return h}}function _e(h,m,_){var S=h.pendingLanes;if(S===0)return 0;var P=0,D=h.suspendedLanes,X=h.pingedLanes;h=h.warmLanes;var le=S&134217727;return le!==0?(S=le&~D,S!==0?P=ke(S):(X&=le,X!==0?P=ke(X):_||(_=le&~h,_!==0&&(P=ke(_))))):(le=S&~D,le!==0?P=ke(le):X!==0?P=ke(X):_||(_=S&~h,_!==0&&(P=ke(_)))),P===0?0:m!==0&&m!==P&&(m&D)===0&&(D=P&-P,_=m&-m,D>=_||D===32&&(_&4194048)!==0)?m:P}function ue(h,m){return(h.pendingLanes&~(h.suspendedLanes&~h.pingedLanes)&m)===0}function ot(h,m){switch(h){case 1:case 2:case 4:case 8:case 64:return m+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return m+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function xt(){var h=ze;return ze<<=1,(ze&62914560)===0&&(ze=4194304),h}function Et(h){for(var m=[],_=0;31>_;_++)m.push(h);return m}function Qt(h,m){h.pendingLanes|=m,m!==268435456&&(h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0)}function Ot(h,m,_,S,P,D){var X=h.pendingLanes;h.pendingLanes=_,h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0,h.expiredLanes&=_,h.entangledLanes&=_,h.errorRecoveryDisabledLanes&=_,h.shellSuspendCounter=0;var le=h.entanglements,Te=h.expirationTimes,Ze=h.hiddenUpdates;for(_=X&~_;0<_;){var ct=31-$e(_),ht=1<"u")return null;try{return h.activeElement||h.body}catch{return h.body}}var Ir=/[\n"\\]/g;function jn(h){return h.replace(Ir,function(m){return"\\"+m.charCodeAt(0).toString(16)+" "})}function xo(h,m,_,S,P,D,X,le){h.name="",X!=null&&typeof X!="function"&&typeof X!="symbol"&&typeof X!="boolean"?h.type=X:h.removeAttribute("type"),m!=null?X==="number"?(m===0&&h.value===""||h.value!=m)&&(h.value=""+hn(m)):h.value!==""+hn(m)&&(h.value=""+hn(m)):X!=="submit"&&X!=="reset"||h.removeAttribute("value"),m!=null?Ai(h,X,hn(m)):_!=null?Ai(h,X,hn(_)):S!=null&&h.removeAttribute("value"),P==null&&D!=null&&(h.defaultChecked=!!D),P!=null&&(h.checked=P&&typeof P!="function"&&typeof P!="symbol"),le!=null&&typeof le!="function"&&typeof le!="symbol"&&typeof le!="boolean"?h.name=""+hn(le):h.removeAttribute("name")}function Ln(h,m,_,S,P,D,X,le){if(D!=null&&typeof D!="function"&&typeof D!="symbol"&&typeof D!="boolean"&&(h.type=D),m!=null||_!=null){if(!(D!=="submit"&&D!=="reset"||m!=null)){Jt(h);return}_=_!=null?""+hn(_):"",m=m!=null?""+hn(m):_,le||m===h.value||(h.value=m),h.defaultValue=m}S=S??P,S=typeof S!="function"&&typeof S!="symbol"&&!!S,h.checked=le?h.checked:!!S,h.defaultChecked=!!S,X!=null&&typeof X!="function"&&typeof X!="symbol"&&typeof X!="boolean"&&(h.name=X),Jt(h)}function Ai(h,m,_){m==="number"&&Cr(h.ownerDocument)===h||h.defaultValue===""+_||(h.defaultValue=""+_)}function li(h,m,_,S){if(h=h.options,m){m={};for(var P=0;P<_.length;P++)m["$"+_[P]]=!0;for(_=0;_"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),SE=!1;if(Pl)try{var _g={};Object.defineProperty(_g,"passive",{get:function(){SE=!0}}),window.addEventListener("test",_g,_g),window.removeEventListener("test",_g,_g)}catch{SE=!1}var Zc=null,ME=null,oy=null;function RL(){if(oy)return oy;var h,m=ME,_=m.length,S,P="value"in Zc?Zc.value:Zc.textContent,D=P.length;for(h=0;h<_&&m[h]===P[h];h++);var X=_-h;for(S=1;S<=X&&m[_-S]===P[D-S];S++);return oy=P.slice(h,1=Mg),FL=" ",UL=!1;function zL(h,m){switch(h){case"keyup":return Jte.indexOf(m.keyCode)!==-1;case"keydown":return m.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function BL(h){return h=h.detail,typeof h=="object"&&"data"in h?h.data:null}var Sf=!1;function tne(h,m){switch(h){case"compositionend":return BL(m);case"keypress":return m.which!==32?null:(UL=!0,FL);case"textInput":return h=m.data,h===FL&&UL?null:h;default:return null}}function nne(h,m){if(Sf)return h==="compositionend"||!NE&&zL(h,m)?(h=RL(),oy=ME=Zc=null,Sf=!1,h):null;switch(h){case"paste":return null;case"keypress":if(!(m.ctrlKey||m.altKey||m.metaKey)||m.ctrlKey&&m.altKey){if(m.char&&1=m)return{node:_,offset:m-h};h=S}e:{for(;_;){if(_.nextSibling){_=_.nextSibling;break e}_=_.parentNode}_=void 0}_=XL(_)}}function ZL(h,m){return h&&m?h===m?!0:h&&h.nodeType===3?!1:m&&m.nodeType===3?ZL(h,m.parentNode):"contains"in h?h.contains(m):h.compareDocumentPosition?!!(h.compareDocumentPosition(m)&16):!1:!1}function QL(h){h=h!=null&&h.ownerDocument!=null&&h.ownerDocument.defaultView!=null?h.ownerDocument.defaultView:window;for(var m=Cr(h.document);m instanceof h.HTMLIFrameElement;){try{var _=typeof m.contentWindow.location.href=="string"}catch{_=!1}if(_)h=m.contentWindow;else break;m=Cr(h.document)}return m}function RE(h){var m=h&&h.nodeName&&h.nodeName.toLowerCase();return m&&(m==="input"&&(h.type==="text"||h.type==="search"||h.type==="tel"||h.type==="url"||h.type==="password")||m==="textarea"||h.contentEditable==="true")}var une=Pl&&"documentMode"in document&&11>=document.documentMode,Mf=null,jE=null,Cg=null,DE=!1;function JL(h,m,_){var S=_.window===_?_.document:_.nodeType===9?_:_.ownerDocument;DE||Mf==null||Mf!==Cr(S)||(S=Mf,"selectionStart"in S&&RE(S)?S={start:S.selectionStart,end:S.selectionEnd}:(S=(S.ownerDocument&&S.ownerDocument.defaultView||window).getSelection(),S={anchorNode:S.anchorNode,anchorOffset:S.anchorOffset,focusNode:S.focusNode,focusOffset:S.focusOffset}),Cg&&Tg(Cg,S)||(Cg=S,S=ev(jE,"onSelect"),0>=X,P-=X,Wo=1<<32-$e(m)+P|_<rn?(vn=It,It=null):vn=It.sibling;var Cn=Je(Be,It,Xe[rn],dt);if(Cn===null){It===null&&(It=vn);break}h&&It&&Cn.alternate===null&&m(Be,It),je=D(Cn,je,rn),Tn===null?zt=Cn:Tn.sibling=Cn,Tn=Cn,It=vn}if(rn===Xe.length)return _(Be,It),wn&&jl(Be,rn),zt;if(It===null){for(;rnrn?(vn=It,It=null):vn=It.sibling;var yu=Je(Be,It,Cn.value,dt);if(yu===null){It===null&&(It=vn);break}h&&It&&yu.alternate===null&&m(Be,It),je=D(yu,je,rn),Tn===null?zt=yu:Tn.sibling=yu,Tn=yu,It=vn}if(Cn.done)return _(Be,It),wn&&jl(Be,rn),zt;if(It===null){for(;!Cn.done;rn++,Cn=Xe.next())Cn=ht(Be,Cn.value,dt),Cn!==null&&(je=D(Cn,je,rn),Tn===null?zt=Cn:Tn.sibling=Cn,Tn=Cn);return wn&&jl(Be,rn),zt}for(It=S(It);!Cn.done;rn++,Cn=Xe.next())Cn=st(It,Be,rn,Cn.value,dt),Cn!==null&&(h&&Cn.alternate!==null&&It.delete(Cn.key===null?rn:Cn.key),je=D(Cn,je,rn),Tn===null?zt=Cn:Tn.sibling=Cn,Tn=Cn);return h&&It.forEach(function(kre){return m(Be,kre)}),wn&&jl(Be,rn),zt}function Wn(Be,je,Xe,dt){if(typeof Xe=="object"&&Xe!==null&&Xe.type===v&&Xe.key===null&&(Xe=Xe.props.children),typeof Xe=="object"&&Xe!==null){switch(Xe.$$typeof){case p:e:{for(var zt=Xe.key;je!==null;){if(je.key===zt){if(zt=Xe.type,zt===v){if(je.tag===7){_(Be,je.sibling),dt=P(je,Xe.props.children),dt.return=Be,Be=dt;break e}}else if(je.elementType===zt||typeof zt=="object"&&zt!==null&&zt.$$typeof===U&&$d(zt)===je.type){_(Be,je.sibling),dt=P(je,Xe.props),Dg(dt,Xe),dt.return=Be,Be=dt;break e}_(Be,je);break}else m(Be,je);je=je.sibling}Xe.type===v?(dt=Fd(Xe.props.children,Be.mode,dt,Xe.key),dt.return=Be,Be=dt):(dt=xy(Xe.type,Xe.key,Xe.props,null,Be.mode,dt),Dg(dt,Xe),dt.return=Be,Be=dt)}return X(Be);case x:e:{for(zt=Xe.key;je!==null;){if(je.key===zt)if(je.tag===4&&je.stateNode.containerInfo===Xe.containerInfo&&je.stateNode.implementation===Xe.implementation){_(Be,je.sibling),dt=P(je,Xe.children||[]),dt.return=Be,Be=dt;break e}else{_(Be,je);break}else m(Be,je);je=je.sibling}dt=BE(Xe,Be.mode,dt),dt.return=Be,Be=dt}return X(Be);case U:return Xe=$d(Xe),Wn(Be,je,Xe,dt)}if(W(Xe))return Dt(Be,je,Xe,dt);if(L(Xe)){if(zt=L(Xe),typeof zt!="function")throw Error(r(150));return Xe=zt.call(Xe),Gt(Be,je,Xe,dt)}if(typeof Xe.then=="function")return Wn(Be,je,My(Xe),dt);if(Xe.$$typeof===w)return Wn(Be,je,vy(Be,Xe),dt);Ey(Be,Xe)}return typeof Xe=="string"&&Xe!==""||typeof Xe=="number"||typeof Xe=="bigint"?(Xe=""+Xe,je!==null&&je.tag===6?(_(Be,je.sibling),dt=P(je,Xe),dt.return=Be,Be=dt):(_(Be,je),dt=zE(Xe,Be.mode,dt),dt.return=Be,Be=dt),X(Be)):_(Be,je)}return function(Be,je,Xe,dt){try{jg=0;var zt=Wn(Be,je,Xe,dt);return Of=null,zt}catch(It){if(It===Df||It===wy)throw It;var Tn=cs(29,It,null,Be.mode);return Tn.lanes=dt,Tn.return=Be,Tn}finally{}}}var Gd=w4(!0),S4=w4(!1),nu=!1;function JE(h){h.updateQueue={baseState:h.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function e2(h,m){h=h.updateQueue,m.updateQueue===h&&(m.updateQueue={baseState:h.baseState,firstBaseUpdate:h.firstBaseUpdate,lastBaseUpdate:h.lastBaseUpdate,shared:h.shared,callbacks:null})}function ru(h){return{lane:h,tag:0,payload:null,callback:null,next:null}}function iu(h,m,_){var S=h.updateQueue;if(S===null)return null;if(S=S.shared,(Pn&2)!==0){var P=S.pending;return P===null?m.next=m:(m.next=P.next,P.next=m),S.pending=m,m=gy(h),s4(h,null,_),m}return py(h,S,m,_),gy(h)}function Og(h,m,_){if(m=m.updateQueue,m!==null&&(m=m.shared,(_&4194048)!==0)){var S=m.lanes;S&=h.pendingLanes,_|=S,m.lanes=_,On(h,_)}}function t2(h,m){var _=h.updateQueue,S=h.alternate;if(S!==null&&(S=S.updateQueue,_===S)){var P=null,D=null;if(_=_.firstBaseUpdate,_!==null){do{var X={lane:_.lane,tag:_.tag,payload:_.payload,callback:null,next:null};D===null?P=D=X:D=D.next=X,_=_.next}while(_!==null);D===null?P=D=m:D=D.next=m}else P=D=m;_={baseState:S.baseState,firstBaseUpdate:P,lastBaseUpdate:D,shared:S.shared,callbacks:S.callbacks},h.updateQueue=_;return}h=_.lastBaseUpdate,h===null?_.firstBaseUpdate=m:h.next=m,_.lastBaseUpdate=m}var n2=!1;function Lg(){if(n2){var h=jf;if(h!==null)throw h}}function Ig(h,m,_,S){n2=!1;var P=h.updateQueue;nu=!1;var D=P.firstBaseUpdate,X=P.lastBaseUpdate,le=P.shared.pending;if(le!==null){P.shared.pending=null;var Te=le,Ze=Te.next;Te.next=null,X===null?D=Ze:X.next=Ze,X=Te;var ct=h.alternate;ct!==null&&(ct=ct.updateQueue,le=ct.lastBaseUpdate,le!==X&&(le===null?ct.firstBaseUpdate=Ze:le.next=Ze,ct.lastBaseUpdate=Te))}if(D!==null){var ht=P.baseState;X=0,ct=Ze=Te=null,le=D;do{var Je=le.lane&-536870913,st=Je!==le.lane;if(st?(yn&Je)===Je:(S&Je)===Je){Je!==0&&Je===Rf&&(n2=!0),ct!==null&&(ct=ct.next={lane:0,tag:le.tag,payload:le.payload,callback:null,next:null});e:{var Dt=h,Gt=le;Je=m;var Wn=_;switch(Gt.tag){case 1:if(Dt=Gt.payload,typeof Dt=="function"){ht=Dt.call(Wn,ht,Je);break e}ht=Dt;break e;case 3:Dt.flags=Dt.flags&-65537|128;case 0:if(Dt=Gt.payload,Je=typeof Dt=="function"?Dt.call(Wn,ht,Je):Dt,Je==null)break e;ht=f({},ht,Je);break e;case 2:nu=!0}}Je=le.callback,Je!==null&&(h.flags|=64,st&&(h.flags|=8192),st=P.callbacks,st===null?P.callbacks=[Je]:st.push(Je))}else st={lane:Je,tag:le.tag,payload:le.payload,callback:le.callback,next:null},ct===null?(Ze=ct=st,Te=ht):ct=ct.next=st,X|=Je;if(le=le.next,le===null){if(le=P.shared.pending,le===null)break;st=le,le=st.next,st.next=null,P.lastBaseUpdate=st,P.shared.pending=null}}while(!0);ct===null&&(Te=ht),P.baseState=Te,P.firstBaseUpdate=Ze,P.lastBaseUpdate=ct,D===null&&(P.shared.lanes=0),cu|=X,h.lanes=X,h.memoizedState=ht}}function M4(h,m){if(typeof h!="function")throw Error(r(191,h));h.call(m)}function E4(h,m){var _=h.callbacks;if(_!==null)for(h.callbacks=null,h=0;h<_.length;h++)M4(_[h],m)}var Lf=z(null),Ay=z(0);function A4(h,m){h=Vl,J(Ay,h),J(Lf,m),Vl=h|m.baseLanes}function r2(){J(Ay,Vl),J(Lf,Lf.current)}function i2(){Vl=Ay.current,$(Lf),$(Ay)}var us=z(null),Ls=null;function au(h){var m=h.alternate;J(Fr,Fr.current&1),J(us,h),Ls===null&&(m===null||Lf.current!==null||m.memoizedState!==null)&&(Ls=h)}function a2(h){J(Fr,Fr.current),J(us,h),Ls===null&&(Ls=h)}function T4(h){h.tag===22?(J(Fr,Fr.current),J(us,h),Ls===null&&(Ls=h)):su()}function su(){J(Fr,Fr.current),J(us,us.current)}function ds(h){$(us),Ls===h&&(Ls=null),$(Fr)}var Fr=z(0);function Ty(h){for(var m=h;m!==null;){if(m.tag===13){var _=m.memoizedState;if(_!==null&&(_=_.dehydrated,_===null||dA(_)||hA(_)))return m}else if(m.tag===19&&(m.memoizedProps.revealOrder==="forwards"||m.memoizedProps.revealOrder==="backwards"||m.memoizedProps.revealOrder==="unstable_legacy-backwards"||m.memoizedProps.revealOrder==="together")){if((m.flags&128)!==0)return m}else if(m.child!==null){m.child.return=m,m=m.child;continue}if(m===h)break;for(;m.sibling===null;){if(m.return===null||m.return===h)return null;m=m.return}m.sibling.return=m.return,m=m.sibling}return null}var Ll=0,nn=null,Vn=null,Kr=null,Cy=!1,If=!1,Wd=!1,Ny=0,Fg=0,Ff=null,vne=0;function Nr(){throw Error(r(321))}function s2(h,m){if(m===null)return!1;for(var _=0;_D?D:8;var X=K.T,le={};K.T=le,_2(h,!1,m,_);try{var Te=P(),Ze=K.S;if(Ze!==null&&Ze(le,Te),Te!==null&&typeof Te=="object"&&typeof Te.then=="function"){var ct=yne(Te,S);zg(h,m,ct,ms(h))}else zg(h,m,S,ms(h))}catch(ht){zg(h,m,{then:function(){},status:"rejected",reason:ht},ms())}finally{G.p=D,X!==null&&le.types!==null&&(X.types=le.types),K.T=X}}function Ene(){}function y2(h,m,_,S){if(h.tag!==5)throw Error(r(476));var P=rI(h).queue;nI(h,P,m,ee,_===null?Ene:function(){return iI(h),_(S)})}function rI(h){var m=h.memoizedState;if(m!==null)return m;m={memoizedState:ee,baseState:ee,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Il,lastRenderedState:ee},next:null};var _={};return m.next={memoizedState:_,baseState:_,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Il,lastRenderedState:_},next:null},h.memoizedState=m,h=h.alternate,h!==null&&(h.memoizedState=m),m}function iI(h){var m=rI(h);m.next===null&&(m=h.alternate.memoizedState),zg(h,m.next.queue,{},ms())}function v2(){return $i(rx)}function aI(){return Ur().memoizedState}function sI(){return Ur().memoizedState}function Ane(h){for(var m=h.return;m!==null;){switch(m.tag){case 24:case 3:var _=ms();h=ru(_);var S=iu(m,h,_);S!==null&&(Ba(S,m,_),Og(S,m,_)),m={cache:XE()},h.payload=m;return}m=m.return}}function Tne(h,m,_){var S=ms();_={lane:S,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null},Oy(h)?lI(m,_):(_=FE(h,m,_,S),_!==null&&(Ba(_,h,S),cI(_,m,S)))}function oI(h,m,_){var S=ms();zg(h,m,_,S)}function zg(h,m,_,S){var P={lane:S,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null};if(Oy(h))lI(m,P);else{var D=h.alternate;if(h.lanes===0&&(D===null||D.lanes===0)&&(D=m.lastRenderedReducer,D!==null))try{var X=m.lastRenderedState,le=D(X,_);if(P.hasEagerState=!0,P.eagerState=le,ls(le,X))return py(h,m,P,0),er===null&&my(),!1}catch{}finally{}if(_=FE(h,m,P,S),_!==null)return Ba(_,h,S),cI(_,m,S),!0}return!1}function _2(h,m,_,S){if(S={lane:2,revertLane:J2(),gesture:null,action:S,hasEagerState:!1,eagerState:null,next:null},Oy(h)){if(m)throw Error(r(479))}else m=FE(h,_,S,2),m!==null&&Ba(m,h,2)}function Oy(h){var m=h.alternate;return h===nn||m!==null&&m===nn}function lI(h,m){If=Cy=!0;var _=h.pending;_===null?m.next=m:(m.next=_.next,_.next=m),h.pending=m}function cI(h,m,_){if((_&4194048)!==0){var S=m.lanes;S&=h.pendingLanes,_|=S,m.lanes=_,On(h,_)}}var Bg={readContext:$i,use:Py,useCallback:Nr,useContext:Nr,useEffect:Nr,useImperativeHandle:Nr,useLayoutEffect:Nr,useInsertionEffect:Nr,useMemo:Nr,useReducer:Nr,useRef:Nr,useState:Nr,useDebugValue:Nr,useDeferredValue:Nr,useTransition:Nr,useSyncExternalStore:Nr,useId:Nr,useHostTransitionStatus:Nr,useFormState:Nr,useActionState:Nr,useOptimistic:Nr,useMemoCache:Nr,useCacheRefresh:Nr};Bg.useEffectEvent=Nr;var uI={readContext:$i,use:Py,useCallback:function(h,m){return ya().memoizedState=[h,m===void 0?null:m],h},useContext:$i,useEffect:q4,useImperativeHandle:function(h,m,_){_=_!=null?_.concat([h]):null,jy(4194308,4,Z4.bind(null,m,h),_)},useLayoutEffect:function(h,m){return jy(4194308,4,h,m)},useInsertionEffect:function(h,m){jy(4,2,h,m)},useMemo:function(h,m){var _=ya();m=m===void 0?null:m;var S=h();if(Wd){tt(!0);try{h()}finally{tt(!1)}}return _.memoizedState=[S,m],S},useReducer:function(h,m,_){var S=ya();if(_!==void 0){var P=_(m);if(Wd){tt(!0);try{_(m)}finally{tt(!1)}}}else P=m;return S.memoizedState=S.baseState=P,h={pending:null,lanes:0,dispatch:null,lastRenderedReducer:h,lastRenderedState:P},S.queue=h,h=h.dispatch=Tne.bind(null,nn,h),[S.memoizedState,h]},useRef:function(h){var m=ya();return h={current:h},m.memoizedState=h},useState:function(h){h=m2(h);var m=h.queue,_=oI.bind(null,nn,m);return m.dispatch=_,[h.memoizedState,_]},useDebugValue:x2,useDeferredValue:function(h,m){var _=ya();return b2(_,h,m)},useTransition:function(){var h=m2(!1);return h=nI.bind(null,nn,h.queue,!0,!1),ya().memoizedState=h,[!1,h]},useSyncExternalStore:function(h,m,_){var S=nn,P=ya();if(wn){if(_===void 0)throw Error(r(407));_=_()}else{if(_=m(),er===null)throw Error(r(349));(yn&127)!==0||P4(S,m,_)}P.memoizedState=_;var D={value:_,getSnapshot:m};return P.queue=D,q4(j4.bind(null,S,D,h),[h]),S.flags|=2048,Uf(9,{destroy:void 0},R4.bind(null,S,D,_,m),null),_},useId:function(){var h=ya(),m=er.identifierPrefix;if(wn){var _=qo,S=Wo;_=(S&~(1<<32-$e(S)-1)).toString(32)+_,m="_"+m+"R_"+_,_=Ny++,0<_&&(m+="H"+_.toString(32)),m+="_"}else _=vne++,m="_"+m+"r_"+_.toString(32)+"_";return h.memoizedState=m},useHostTransitionStatus:v2,useFormState:H4,useActionState:H4,useOptimistic:function(h){var m=ya();m.memoizedState=m.baseState=h;var _={pending:null,lanes:0,dispatch:null,lastRenderedReducer:null,lastRenderedState:null};return m.queue=_,m=_2.bind(null,nn,!0,_),_.dispatch=m,[h,m]},useMemoCache:d2,useCacheRefresh:function(){return ya().memoizedState=Ane.bind(null,nn)},useEffectEvent:function(h){var m=ya(),_={impl:h};return m.memoizedState=_,function(){if((Pn&2)!==0)throw Error(r(440));return _.impl.apply(void 0,arguments)}}},w2={readContext:$i,use:Py,useCallback:J4,useContext:$i,useEffect:g2,useImperativeHandle:Q4,useInsertionEffect:X4,useLayoutEffect:Y4,useMemo:eI,useReducer:Ry,useRef:W4,useState:function(){return Ry(Il)},useDebugValue:x2,useDeferredValue:function(h,m){var _=Ur();return tI(_,Vn.memoizedState,h,m)},useTransition:function(){var h=Ry(Il)[0],m=Ur().memoizedState;return[typeof h=="boolean"?h:Ug(h),m]},useSyncExternalStore:k4,useId:aI,useHostTransitionStatus:v2,useFormState:$4,useActionState:$4,useOptimistic:function(h,m){var _=Ur();return L4(_,Vn,h,m)},useMemoCache:d2,useCacheRefresh:sI};w2.useEffectEvent=K4;var dI={readContext:$i,use:Py,useCallback:J4,useContext:$i,useEffect:g2,useImperativeHandle:Q4,useInsertionEffect:X4,useLayoutEffect:Y4,useMemo:eI,useReducer:f2,useRef:W4,useState:function(){return f2(Il)},useDebugValue:x2,useDeferredValue:function(h,m){var _=Ur();return Vn===null?b2(_,h,m):tI(_,Vn.memoizedState,h,m)},useTransition:function(){var h=f2(Il)[0],m=Ur().memoizedState;return[typeof h=="boolean"?h:Ug(h),m]},useSyncExternalStore:k4,useId:aI,useHostTransitionStatus:v2,useFormState:G4,useActionState:G4,useOptimistic:function(h,m){var _=Ur();return Vn!==null?L4(_,Vn,h,m):(_.baseState=h,[h,_.queue.dispatch])},useMemoCache:d2,useCacheRefresh:sI};dI.useEffectEvent=K4;function S2(h,m,_,S){m=h.memoizedState,_=_(S,m),_=_==null?m:f({},m,_),h.memoizedState=_,h.lanes===0&&(h.updateQueue.baseState=_)}var M2={enqueueSetState:function(h,m,_){h=h._reactInternals;var S=ms(),P=ru(S);P.payload=m,_!=null&&(P.callback=_),m=iu(h,P,S),m!==null&&(Ba(m,h,S),Og(m,h,S))},enqueueReplaceState:function(h,m,_){h=h._reactInternals;var S=ms(),P=ru(S);P.tag=1,P.payload=m,_!=null&&(P.callback=_),m=iu(h,P,S),m!==null&&(Ba(m,h,S),Og(m,h,S))},enqueueForceUpdate:function(h,m){h=h._reactInternals;var _=ms(),S=ru(_);S.tag=2,m!=null&&(S.callback=m),m=iu(h,S,_),m!==null&&(Ba(m,h,_),Og(m,h,_))}};function hI(h,m,_,S,P,D,X){return h=h.stateNode,typeof h.shouldComponentUpdate=="function"?h.shouldComponentUpdate(S,D,X):m.prototype&&m.prototype.isPureReactComponent?!Tg(_,S)||!Tg(P,D):!0}function fI(h,m,_,S){h=m.state,typeof m.componentWillReceiveProps=="function"&&m.componentWillReceiveProps(_,S),typeof m.UNSAFE_componentWillReceiveProps=="function"&&m.UNSAFE_componentWillReceiveProps(_,S),m.state!==h&&M2.enqueueReplaceState(m,m.state,null)}function qd(h,m){var _=m;if("ref"in m){_={};for(var S in m)S!=="ref"&&(_[S]=m[S])}if(h=h.defaultProps){_===m&&(_=f({},_));for(var P in h)_[P]===void 0&&(_[P]=h[P])}return _}function mI(h){fy(h)}function pI(h){console.error(h)}function gI(h){fy(h)}function Ly(h,m){try{var _=h.onUncaughtError;_(m.value,{componentStack:m.stack})}catch(S){setTimeout(function(){throw S})}}function xI(h,m,_){try{var S=h.onCaughtError;S(_.value,{componentStack:_.stack,errorBoundary:m.tag===1?m.stateNode:null})}catch(P){setTimeout(function(){throw P})}}function E2(h,m,_){return _=ru(_),_.tag=3,_.payload={element:null},_.callback=function(){Ly(h,m)},_}function bI(h){return h=ru(h),h.tag=3,h}function yI(h,m,_,S){var P=_.type.getDerivedStateFromError;if(typeof P=="function"){var D=S.value;h.payload=function(){return P(D)},h.callback=function(){xI(m,_,S)}}var X=_.stateNode;X!==null&&typeof X.componentDidCatch=="function"&&(h.callback=function(){xI(m,_,S),typeof P!="function"&&(uu===null?uu=new Set([this]):uu.add(this));var le=S.stack;this.componentDidCatch(S.value,{componentStack:le!==null?le:""})})}function Cne(h,m,_,S,P){if(_.flags|=32768,S!==null&&typeof S=="object"&&typeof S.then=="function"){if(m=_.alternate,m!==null&&Pf(m,_,P,!0),_=us.current,_!==null){switch(_.tag){case 31:case 13:return Ls===null?Ky():_.alternate===null&&kr===0&&(kr=3),_.flags&=-257,_.flags|=65536,_.lanes=P,S===Sy?_.flags|=16384:(m=_.updateQueue,m===null?_.updateQueue=new Set([S]):m.add(S),Y2(h,S,P)),!1;case 22:return _.flags|=65536,S===Sy?_.flags|=16384:(m=_.updateQueue,m===null?(m={transitions:null,markerInstances:null,retryQueue:new Set([S])},_.updateQueue=m):(_=m.retryQueue,_===null?m.retryQueue=new Set([S]):_.add(S)),Y2(h,S,P)),!1}throw Error(r(435,_.tag))}return Y2(h,S,P),Ky(),!1}if(wn)return m=us.current,m!==null?((m.flags&65536)===0&&(m.flags|=256),m.flags|=65536,m.lanes=P,S!==VE&&(h=Error(r(422),{cause:S}),kg(Rs(h,_)))):(S!==VE&&(m=Error(r(423),{cause:S}),kg(Rs(m,_))),h=h.current.alternate,h.flags|=65536,P&=-P,h.lanes|=P,S=Rs(S,_),P=E2(h.stateNode,S,P),t2(h,P),kr!==4&&(kr=2)),!1;var D=Error(r(520),{cause:S});if(D=Rs(D,_),Xg===null?Xg=[D]:Xg.push(D),kr!==4&&(kr=2),m===null)return!0;S=Rs(S,_),_=m;do{switch(_.tag){case 3:return _.flags|=65536,h=P&-P,_.lanes|=h,h=E2(_.stateNode,S,h),t2(_,h),!1;case 1:if(m=_.type,D=_.stateNode,(_.flags&128)===0&&(typeof m.getDerivedStateFromError=="function"||D!==null&&typeof D.componentDidCatch=="function"&&(uu===null||!uu.has(D))))return _.flags|=65536,P&=-P,_.lanes|=P,P=bI(P),yI(P,h,_,S),t2(_,P),!1}_=_.return}while(_!==null);return!1}var A2=Error(r(461)),Xr=!1;function Vi(h,m,_,S){m.child=h===null?S4(m,null,_,S):Gd(m,h.child,_,S)}function vI(h,m,_,S,P){_=_.render;var D=m.ref;if("ref"in S){var X={};for(var le in S)le!=="ref"&&(X[le]=S[le])}else X=S;return Bd(m),S=o2(h,m,_,X,D,P),le=l2(),h!==null&&!Xr?(c2(h,m,P),Fl(h,m,P)):(wn&&le&&HE(m),m.flags|=1,Vi(h,m,S,P),m.child)}function _I(h,m,_,S,P){if(h===null){var D=_.type;return typeof D=="function"&&!UE(D)&&D.defaultProps===void 0&&_.compare===null?(m.tag=15,m.type=D,wI(h,m,D,S,P)):(h=xy(_.type,null,S,m,m.mode,P),h.ref=m.ref,h.return=m,m.child=h)}if(D=h.child,!D2(h,P)){var X=D.memoizedProps;if(_=_.compare,_=_!==null?_:Tg,_(X,S)&&h.ref===m.ref)return Fl(h,m,P)}return m.flags|=1,h=Rl(D,S),h.ref=m.ref,h.return=m,m.child=h}function wI(h,m,_,S,P){if(h!==null){var D=h.memoizedProps;if(Tg(D,S)&&h.ref===m.ref)if(Xr=!1,m.pendingProps=S=D,D2(h,P))(h.flags&131072)!==0&&(Xr=!0);else return m.lanes=h.lanes,Fl(h,m,P)}return T2(h,m,_,S,P)}function SI(h,m,_,S){var P=S.children,D=h!==null?h.memoizedState:null;if(h===null&&m.stateNode===null&&(m.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),S.mode==="hidden"){if((m.flags&128)!==0){if(D=D!==null?D.baseLanes|_:_,h!==null){for(S=m.child=h.child,P=0;S!==null;)P=P|S.lanes|S.childLanes,S=S.sibling;S=P&~D}else S=0,m.child=null;return MI(h,m,D,_,S)}if((_&536870912)!==0)m.memoizedState={baseLanes:0,cachePool:null},h!==null&&_y(m,D!==null?D.cachePool:null),D!==null?A4(m,D):r2(),T4(m);else return S=m.lanes=536870912,MI(h,m,D!==null?D.baseLanes|_:_,_,S)}else D!==null?(_y(m,D.cachePool),A4(m,D),su(),m.memoizedState=null):(h!==null&&_y(m,null),r2(),su());return Vi(h,m,P,_),m.child}function Hg(h,m){return h!==null&&h.tag===22||m.stateNode!==null||(m.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),m.sibling}function MI(h,m,_,S,P){var D=ZE();return D=D===null?null:{parent:qr._currentValue,pool:D},m.memoizedState={baseLanes:_,cachePool:D},h!==null&&_y(m,null),r2(),T4(m),h!==null&&Pf(h,m,S,!0),m.childLanes=P,null}function Iy(h,m){return m=Uy({mode:m.mode,children:m.children},h.mode),m.ref=h.ref,h.child=m,m.return=h,m}function EI(h,m,_){return Gd(m,h.child,null,_),h=Iy(m,m.pendingProps),h.flags|=2,ds(m),m.memoizedState=null,h}function Nne(h,m,_){var S=m.pendingProps,P=(m.flags&128)!==0;if(m.flags&=-129,h===null){if(wn){if(S.mode==="hidden")return h=Iy(m,S),m.lanes=536870912,Hg(null,h);if(a2(m),(h=lr)?(h=I5(h,Os),h=h!==null&&h.data==="&"?h:null,h!==null&&(m.memoizedState={dehydrated:h,treeContext:Qc!==null?{id:Wo,overflow:qo}:null,retryLane:536870912,hydrationErrors:null},_=l4(h),_.return=m,m.child=_,Hi=m,lr=null)):h=null,h===null)throw eu(m);return m.lanes=536870912,null}return Iy(m,S)}var D=h.memoizedState;if(D!==null){var X=D.dehydrated;if(a2(m),P)if(m.flags&256)m.flags&=-257,m=EI(h,m,_);else if(m.memoizedState!==null)m.child=h.child,m.flags|=128,m=null;else throw Error(r(558));else if(Xr||Pf(h,m,_,!1),P=(_&h.childLanes)!==0,Xr||P){if(S=er,S!==null&&(X=Mi(S,_),X!==0&&X!==D.retryLane))throw D.retryLane=X,Id(h,X),Ba(S,h,X),A2;Ky(),m=EI(h,m,_)}else h=D.treeContext,lr=Is(X.nextSibling),Hi=m,wn=!0,Jc=null,Os=!1,h!==null&&d4(m,h),m=Iy(m,S),m.flags|=4096;return m}return h=Rl(h.child,{mode:S.mode,children:S.children}),h.ref=m.ref,m.child=h,h.return=m,h}function Fy(h,m){var _=m.ref;if(_===null)h!==null&&h.ref!==null&&(m.flags|=4194816);else{if(typeof _!="function"&&typeof _!="object")throw Error(r(284));(h===null||h.ref!==_)&&(m.flags|=4194816)}}function T2(h,m,_,S,P){return Bd(m),_=o2(h,m,_,S,void 0,P),S=l2(),h!==null&&!Xr?(c2(h,m,P),Fl(h,m,P)):(wn&&S&&HE(m),m.flags|=1,Vi(h,m,_,P),m.child)}function AI(h,m,_,S,P,D){return Bd(m),m.updateQueue=null,_=N4(m,S,_,P),C4(h),S=l2(),h!==null&&!Xr?(c2(h,m,D),Fl(h,m,D)):(wn&&S&&HE(m),m.flags|=1,Vi(h,m,_,D),m.child)}function TI(h,m,_,S,P){if(Bd(m),m.stateNode===null){var D=Tf,X=_.contextType;typeof X=="object"&&X!==null&&(D=$i(X)),D=new _(S,D),m.memoizedState=D.state!==null&&D.state!==void 0?D.state:null,D.updater=M2,m.stateNode=D,D._reactInternals=m,D=m.stateNode,D.props=S,D.state=m.memoizedState,D.refs={},JE(m),X=_.contextType,D.context=typeof X=="object"&&X!==null?$i(X):Tf,D.state=m.memoizedState,X=_.getDerivedStateFromProps,typeof X=="function"&&(S2(m,_,X,S),D.state=m.memoizedState),typeof _.getDerivedStateFromProps=="function"||typeof D.getSnapshotBeforeUpdate=="function"||typeof D.UNSAFE_componentWillMount!="function"&&typeof D.componentWillMount!="function"||(X=D.state,typeof D.componentWillMount=="function"&&D.componentWillMount(),typeof D.UNSAFE_componentWillMount=="function"&&D.UNSAFE_componentWillMount(),X!==D.state&&M2.enqueueReplaceState(D,D.state,null),Ig(m,S,D,P),Lg(),D.state=m.memoizedState),typeof D.componentDidMount=="function"&&(m.flags|=4194308),S=!0}else if(h===null){D=m.stateNode;var le=m.memoizedProps,Te=qd(_,le);D.props=Te;var Ze=D.context,ct=_.contextType;X=Tf,typeof ct=="object"&&ct!==null&&(X=$i(ct));var ht=_.getDerivedStateFromProps;ct=typeof ht=="function"||typeof D.getSnapshotBeforeUpdate=="function",le=m.pendingProps!==le,ct||typeof D.UNSAFE_componentWillReceiveProps!="function"&&typeof D.componentWillReceiveProps!="function"||(le||Ze!==X)&&fI(m,D,S,X),nu=!1;var Je=m.memoizedState;D.state=Je,Ig(m,S,D,P),Lg(),Ze=m.memoizedState,le||Je!==Ze||nu?(typeof ht=="function"&&(S2(m,_,ht,S),Ze=m.memoizedState),(Te=nu||hI(m,_,Te,S,Je,Ze,X))?(ct||typeof D.UNSAFE_componentWillMount!="function"&&typeof D.componentWillMount!="function"||(typeof D.componentWillMount=="function"&&D.componentWillMount(),typeof D.UNSAFE_componentWillMount=="function"&&D.UNSAFE_componentWillMount()),typeof D.componentDidMount=="function"&&(m.flags|=4194308)):(typeof D.componentDidMount=="function"&&(m.flags|=4194308),m.memoizedProps=S,m.memoizedState=Ze),D.props=S,D.state=Ze,D.context=X,S=Te):(typeof D.componentDidMount=="function"&&(m.flags|=4194308),S=!1)}else{D=m.stateNode,e2(h,m),X=m.memoizedProps,ct=qd(_,X),D.props=ct,ht=m.pendingProps,Je=D.context,Ze=_.contextType,Te=Tf,typeof Ze=="object"&&Ze!==null&&(Te=$i(Ze)),le=_.getDerivedStateFromProps,(Ze=typeof le=="function"||typeof D.getSnapshotBeforeUpdate=="function")||typeof D.UNSAFE_componentWillReceiveProps!="function"&&typeof D.componentWillReceiveProps!="function"||(X!==ht||Je!==Te)&&fI(m,D,S,Te),nu=!1,Je=m.memoizedState,D.state=Je,Ig(m,S,D,P),Lg();var st=m.memoizedState;X!==ht||Je!==st||nu||h!==null&&h.dependencies!==null&&yy(h.dependencies)?(typeof le=="function"&&(S2(m,_,le,S),st=m.memoizedState),(ct=nu||hI(m,_,ct,S,Je,st,Te)||h!==null&&h.dependencies!==null&&yy(h.dependencies))?(Ze||typeof D.UNSAFE_componentWillUpdate!="function"&&typeof D.componentWillUpdate!="function"||(typeof D.componentWillUpdate=="function"&&D.componentWillUpdate(S,st,Te),typeof D.UNSAFE_componentWillUpdate=="function"&&D.UNSAFE_componentWillUpdate(S,st,Te)),typeof D.componentDidUpdate=="function"&&(m.flags|=4),typeof D.getSnapshotBeforeUpdate=="function"&&(m.flags|=1024)):(typeof D.componentDidUpdate!="function"||X===h.memoizedProps&&Je===h.memoizedState||(m.flags|=4),typeof D.getSnapshotBeforeUpdate!="function"||X===h.memoizedProps&&Je===h.memoizedState||(m.flags|=1024),m.memoizedProps=S,m.memoizedState=st),D.props=S,D.state=st,D.context=Te,S=ct):(typeof D.componentDidUpdate!="function"||X===h.memoizedProps&&Je===h.memoizedState||(m.flags|=4),typeof D.getSnapshotBeforeUpdate!="function"||X===h.memoizedProps&&Je===h.memoizedState||(m.flags|=1024),S=!1)}return D=S,Fy(h,m),S=(m.flags&128)!==0,D||S?(D=m.stateNode,_=S&&typeof _.getDerivedStateFromError!="function"?null:D.render(),m.flags|=1,h!==null&&S?(m.child=Gd(m,h.child,null,P),m.child=Gd(m,null,_,P)):Vi(h,m,_,P),m.memoizedState=D.state,h=m.child):h=Fl(h,m,P),h}function CI(h,m,_,S){return Ud(),m.flags|=256,Vi(h,m,_,S),m.child}var C2={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function N2(h){return{baseLanes:h,cachePool:x4()}}function k2(h,m,_){return h=h!==null?h.childLanes&~_:0,m&&(h|=fs),h}function NI(h,m,_){var S=m.pendingProps,P=!1,D=(m.flags&128)!==0,X;if((X=D)||(X=h!==null&&h.memoizedState===null?!1:(Fr.current&2)!==0),X&&(P=!0,m.flags&=-129),X=(m.flags&32)!==0,m.flags&=-33,h===null){if(wn){if(P?au(m):su(),(h=lr)?(h=I5(h,Os),h=h!==null&&h.data!=="&"?h:null,h!==null&&(m.memoizedState={dehydrated:h,treeContext:Qc!==null?{id:Wo,overflow:qo}:null,retryLane:536870912,hydrationErrors:null},_=l4(h),_.return=m,m.child=_,Hi=m,lr=null)):h=null,h===null)throw eu(m);return hA(h)?m.lanes=32:m.lanes=536870912,null}var le=S.children;return S=S.fallback,P?(su(),P=m.mode,le=Uy({mode:"hidden",children:le},P),S=Fd(S,P,_,null),le.return=m,S.return=m,le.sibling=S,m.child=le,S=m.child,S.memoizedState=N2(_),S.childLanes=k2(h,X,_),m.memoizedState=C2,Hg(null,S)):(au(m),P2(m,le))}var Te=h.memoizedState;if(Te!==null&&(le=Te.dehydrated,le!==null)){if(D)m.flags&256?(au(m),m.flags&=-257,m=R2(h,m,_)):m.memoizedState!==null?(su(),m.child=h.child,m.flags|=128,m=null):(su(),le=S.fallback,P=m.mode,S=Uy({mode:"visible",children:S.children},P),le=Fd(le,P,_,null),le.flags|=2,S.return=m,le.return=m,S.sibling=le,m.child=S,Gd(m,h.child,null,_),S=m.child,S.memoizedState=N2(_),S.childLanes=k2(h,X,_),m.memoizedState=C2,m=Hg(null,S));else if(au(m),hA(le)){if(X=le.nextSibling&&le.nextSibling.dataset,X)var Ze=X.dgst;X=Ze,S=Error(r(419)),S.stack="",S.digest=X,kg({value:S,source:null,stack:null}),m=R2(h,m,_)}else if(Xr||Pf(h,m,_,!1),X=(_&h.childLanes)!==0,Xr||X){if(X=er,X!==null&&(S=Mi(X,_),S!==0&&S!==Te.retryLane))throw Te.retryLane=S,Id(h,S),Ba(X,h,S),A2;dA(le)||Ky(),m=R2(h,m,_)}else dA(le)?(m.flags|=192,m.child=h.child,m=null):(h=Te.treeContext,lr=Is(le.nextSibling),Hi=m,wn=!0,Jc=null,Os=!1,h!==null&&d4(m,h),m=P2(m,S.children),m.flags|=4096);return m}return P?(su(),le=S.fallback,P=m.mode,Te=h.child,Ze=Te.sibling,S=Rl(Te,{mode:"hidden",children:S.children}),S.subtreeFlags=Te.subtreeFlags&65011712,Ze!==null?le=Rl(Ze,le):(le=Fd(le,P,_,null),le.flags|=2),le.return=m,S.return=m,S.sibling=le,m.child=S,Hg(null,S),S=m.child,le=h.child.memoizedState,le===null?le=N2(_):(P=le.cachePool,P!==null?(Te=qr._currentValue,P=P.parent!==Te?{parent:Te,pool:Te}:P):P=x4(),le={baseLanes:le.baseLanes|_,cachePool:P}),S.memoizedState=le,S.childLanes=k2(h,X,_),m.memoizedState=C2,Hg(h.child,S)):(au(m),_=h.child,h=_.sibling,_=Rl(_,{mode:"visible",children:S.children}),_.return=m,_.sibling=null,h!==null&&(X=m.deletions,X===null?(m.deletions=[h],m.flags|=16):X.push(h)),m.child=_,m.memoizedState=null,_)}function P2(h,m){return m=Uy({mode:"visible",children:m},h.mode),m.return=h,h.child=m}function Uy(h,m){return h=cs(22,h,null,m),h.lanes=0,h}function R2(h,m,_){return Gd(m,h.child,null,_),h=P2(m,m.pendingProps.children),h.flags|=2,m.memoizedState=null,h}function kI(h,m,_){h.lanes|=m;var S=h.alternate;S!==null&&(S.lanes|=m),qE(h.return,m,_)}function j2(h,m,_,S,P,D){var X=h.memoizedState;X===null?h.memoizedState={isBackwards:m,rendering:null,renderingStartTime:0,last:S,tail:_,tailMode:P,treeForkCount:D}:(X.isBackwards=m,X.rendering=null,X.renderingStartTime=0,X.last=S,X.tail=_,X.tailMode=P,X.treeForkCount=D)}function PI(h,m,_){var S=m.pendingProps,P=S.revealOrder,D=S.tail;S=S.children;var X=Fr.current,le=(X&2)!==0;if(le?(X=X&1|2,m.flags|=128):X&=1,J(Fr,X),Vi(h,m,S,_),S=wn?Ng:0,!le&&h!==null&&(h.flags&128)!==0)e:for(h=m.child;h!==null;){if(h.tag===13)h.memoizedState!==null&&kI(h,_,m);else if(h.tag===19)kI(h,_,m);else if(h.child!==null){h.child.return=h,h=h.child;continue}if(h===m)break e;for(;h.sibling===null;){if(h.return===null||h.return===m)break e;h=h.return}h.sibling.return=h.return,h=h.sibling}switch(P){case"forwards":for(_=m.child,P=null;_!==null;)h=_.alternate,h!==null&&Ty(h)===null&&(P=_),_=_.sibling;_=P,_===null?(P=m.child,m.child=null):(P=_.sibling,_.sibling=null),j2(m,!1,P,_,D,S);break;case"backwards":case"unstable_legacy-backwards":for(_=null,P=m.child,m.child=null;P!==null;){if(h=P.alternate,h!==null&&Ty(h)===null){m.child=P;break}h=P.sibling,P.sibling=_,_=P,P=h}j2(m,!0,_,null,D,S);break;case"together":j2(m,!1,null,null,void 0,S);break;default:m.memoizedState=null}return m.child}function Fl(h,m,_){if(h!==null&&(m.dependencies=h.dependencies),cu|=m.lanes,(_&m.childLanes)===0)if(h!==null){if(Pf(h,m,_,!1),(_&m.childLanes)===0)return null}else return null;if(h!==null&&m.child!==h.child)throw Error(r(153));if(m.child!==null){for(h=m.child,_=Rl(h,h.pendingProps),m.child=_,_.return=m;h.sibling!==null;)h=h.sibling,_=_.sibling=Rl(h,h.pendingProps),_.return=m;_.sibling=null}return m.child}function D2(h,m){return(h.lanes&m)!==0?!0:(h=h.dependencies,!!(h!==null&&yy(h)))}function kne(h,m,_){switch(m.tag){case 3:Re(m,m.stateNode.containerInfo),tu(m,qr,h.memoizedState.cache),Ud();break;case 27:case 5:we(m);break;case 4:Re(m,m.stateNode.containerInfo);break;case 10:tu(m,m.type,m.memoizedProps.value);break;case 31:if(m.memoizedState!==null)return m.flags|=128,a2(m),null;break;case 13:var S=m.memoizedState;if(S!==null)return S.dehydrated!==null?(au(m),m.flags|=128,null):(_&m.child.childLanes)!==0?NI(h,m,_):(au(m),h=Fl(h,m,_),h!==null?h.sibling:null);au(m);break;case 19:var P=(h.flags&128)!==0;if(S=(_&m.childLanes)!==0,S||(Pf(h,m,_,!1),S=(_&m.childLanes)!==0),P){if(S)return PI(h,m,_);m.flags|=128}if(P=m.memoizedState,P!==null&&(P.rendering=null,P.tail=null,P.lastEffect=null),J(Fr,Fr.current),S)break;return null;case 22:return m.lanes=0,SI(h,m,_,m.pendingProps);case 24:tu(m,qr,h.memoizedState.cache)}return Fl(h,m,_)}function RI(h,m,_){if(h!==null)if(h.memoizedProps!==m.pendingProps)Xr=!0;else{if(!D2(h,_)&&(m.flags&128)===0)return Xr=!1,kne(h,m,_);Xr=(h.flags&131072)!==0}else Xr=!1,wn&&(m.flags&1048576)!==0&&u4(m,Ng,m.index);switch(m.lanes=0,m.tag){case 16:e:{var S=m.pendingProps;if(h=$d(m.elementType),m.type=h,typeof h=="function")UE(h)?(S=qd(h,S),m.tag=1,m=TI(null,m,h,S,_)):(m.tag=0,m=T2(null,m,h,S,_));else{if(h!=null){var P=h.$$typeof;if(P===A){m.tag=11,m=vI(null,m,h,S,_);break e}else if(P===C){m.tag=14,m=_I(null,m,h,S,_);break e}}throw m=R(h)||h,Error(r(306,m,""))}}return m;case 0:return T2(h,m,m.type,m.pendingProps,_);case 1:return S=m.type,P=qd(S,m.pendingProps),TI(h,m,S,P,_);case 3:e:{if(Re(m,m.stateNode.containerInfo),h===null)throw Error(r(387));S=m.pendingProps;var D=m.memoizedState;P=D.element,e2(h,m),Ig(m,S,null,_);var X=m.memoizedState;if(S=X.cache,tu(m,qr,S),S!==D.cache&&KE(m,[qr],_,!0),Lg(),S=X.element,D.isDehydrated)if(D={element:S,isDehydrated:!1,cache:X.cache},m.updateQueue.baseState=D,m.memoizedState=D,m.flags&256){m=CI(h,m,S,_);break e}else if(S!==P){P=Rs(Error(r(424)),m),kg(P),m=CI(h,m,S,_);break e}else{switch(h=m.stateNode.containerInfo,h.nodeType){case 9:h=h.body;break;default:h=h.nodeName==="HTML"?h.ownerDocument.body:h}for(lr=Is(h.firstChild),Hi=m,wn=!0,Jc=null,Os=!0,_=S4(m,null,S,_),m.child=_;_;)_.flags=_.flags&-3|4096,_=_.sibling}else{if(Ud(),S===P){m=Fl(h,m,_);break e}Vi(h,m,S,_)}m=m.child}return m;case 26:return Fy(h,m),h===null?(_=$5(m.type,null,m.pendingProps,null))?m.memoizedState=_:wn||(_=m.type,h=m.pendingProps,S=tv(fe.current).createElement(_),S[yr]=m,S[Lr]=h,Gi(S,_,h),Le(S),m.stateNode=S):m.memoizedState=$5(m.type,h.memoizedProps,m.pendingProps,h.memoizedState),null;case 27:return we(m),h===null&&wn&&(S=m.stateNode=z5(m.type,m.pendingProps,fe.current),Hi=m,Os=!0,P=lr,mu(m.type)?(fA=P,lr=Is(S.firstChild)):lr=P),Vi(h,m,m.pendingProps.children,_),Fy(h,m),h===null&&(m.flags|=4194304),m.child;case 5:return h===null&&wn&&((P=S=lr)&&(S=sre(S,m.type,m.pendingProps,Os),S!==null?(m.stateNode=S,Hi=m,lr=Is(S.firstChild),Os=!1,P=!0):P=!1),P||eu(m)),we(m),P=m.type,D=m.pendingProps,X=h!==null?h.memoizedProps:null,S=D.children,lA(P,D)?S=null:X!==null&&lA(P,X)&&(m.flags|=32),m.memoizedState!==null&&(P=o2(h,m,_ne,null,null,_),rx._currentValue=P),Fy(h,m),Vi(h,m,S,_),m.child;case 6:return h===null&&wn&&((h=_=lr)&&(_=ore(_,m.pendingProps,Os),_!==null?(m.stateNode=_,Hi=m,lr=null,h=!0):h=!1),h||eu(m)),null;case 13:return NI(h,m,_);case 4:return Re(m,m.stateNode.containerInfo),S=m.pendingProps,h===null?m.child=Gd(m,null,S,_):Vi(h,m,S,_),m.child;case 11:return vI(h,m,m.type,m.pendingProps,_);case 7:return Vi(h,m,m.pendingProps,_),m.child;case 8:return Vi(h,m,m.pendingProps.children,_),m.child;case 12:return Vi(h,m,m.pendingProps.children,_),m.child;case 10:return S=m.pendingProps,tu(m,m.type,S.value),Vi(h,m,S.children,_),m.child;case 9:return P=m.type._context,S=m.pendingProps.children,Bd(m),P=$i(P),S=S(P),m.flags|=1,Vi(h,m,S,_),m.child;case 14:return _I(h,m,m.type,m.pendingProps,_);case 15:return wI(h,m,m.type,m.pendingProps,_);case 19:return PI(h,m,_);case 31:return Nne(h,m,_);case 22:return SI(h,m,_,m.pendingProps);case 24:return Bd(m),S=$i(qr),h===null?(P=ZE(),P===null&&(P=er,D=XE(),P.pooledCache=D,D.refCount++,D!==null&&(P.pooledCacheLanes|=_),P=D),m.memoizedState={parent:S,cache:P},JE(m),tu(m,qr,P)):((h.lanes&_)!==0&&(e2(h,m),Ig(m,null,null,_),Lg()),P=h.memoizedState,D=m.memoizedState,P.parent!==S?(P={parent:S,cache:S},m.memoizedState=P,m.lanes===0&&(m.memoizedState=m.updateQueue.baseState=P),tu(m,qr,S)):(S=D.cache,tu(m,qr,S),S!==P.cache&&KE(m,[qr],_,!0))),Vi(h,m,m.pendingProps.children,_),m.child;case 29:throw m.pendingProps}throw Error(r(156,m.tag))}function Ul(h){h.flags|=4}function O2(h,m,_,S,P){if((m=(h.mode&32)!==0)&&(m=!1),m){if(h.flags|=16777216,(P&335544128)===P)if(h.stateNode.complete)h.flags|=8192;else if(a5())h.flags|=8192;else throw Vd=Sy,QE}else h.flags&=-16777217}function jI(h,m){if(m.type!=="stylesheet"||(m.state.loading&4)!==0)h.flags&=-16777217;else if(h.flags|=16777216,!K5(m))if(a5())h.flags|=8192;else throw Vd=Sy,QE}function zy(h,m){m!==null&&(h.flags|=4),h.flags&16384&&(m=h.tag!==22?xt():536870912,h.lanes|=m,$f|=m)}function $g(h,m){if(!wn)switch(h.tailMode){case"hidden":m=h.tail;for(var _=null;m!==null;)m.alternate!==null&&(_=m),m=m.sibling;_===null?h.tail=null:_.sibling=null;break;case"collapsed":_=h.tail;for(var S=null;_!==null;)_.alternate!==null&&(S=_),_=_.sibling;S===null?m||h.tail===null?h.tail=null:h.tail.sibling=null:S.sibling=null}}function cr(h){var m=h.alternate!==null&&h.alternate.child===h.child,_=0,S=0;if(m)for(var P=h.child;P!==null;)_|=P.lanes|P.childLanes,S|=P.subtreeFlags&65011712,S|=P.flags&65011712,P.return=h,P=P.sibling;else for(P=h.child;P!==null;)_|=P.lanes|P.childLanes,S|=P.subtreeFlags,S|=P.flags,P.return=h,P=P.sibling;return h.subtreeFlags|=S,h.childLanes=_,m}function Pne(h,m,_){var S=m.pendingProps;switch($E(m),m.tag){case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return cr(m),null;case 1:return cr(m),null;case 3:return _=m.stateNode,S=null,h!==null&&(S=h.memoizedState.cache),m.memoizedState.cache!==S&&(m.flags|=2048),Ol(qr),re(),_.pendingContext&&(_.context=_.pendingContext,_.pendingContext=null),(h===null||h.child===null)&&(kf(m)?Ul(m):h===null||h.memoizedState.isDehydrated&&(m.flags&256)===0||(m.flags|=1024,GE())),cr(m),null;case 26:var P=m.type,D=m.memoizedState;return h===null?(Ul(m),D!==null?(cr(m),jI(m,D)):(cr(m),O2(m,P,null,S,_))):D?D!==h.memoizedState?(Ul(m),cr(m),jI(m,D)):(cr(m),m.flags&=-16777217):(h=h.memoizedProps,h!==S&&Ul(m),cr(m),O2(m,P,h,S,_)),null;case 27:if(Oe(m),_=fe.current,P=m.type,h!==null&&m.stateNode!=null)h.memoizedProps!==S&&Ul(m);else{if(!S){if(m.stateNode===null)throw Error(r(166));return cr(m),null}h=ie.current,kf(m)?h4(m):(h=z5(P,S,_),m.stateNode=h,Ul(m))}return cr(m),null;case 5:if(Oe(m),P=m.type,h!==null&&m.stateNode!=null)h.memoizedProps!==S&&Ul(m);else{if(!S){if(m.stateNode===null)throw Error(r(166));return cr(m),null}if(D=ie.current,kf(m))h4(m);else{var X=tv(fe.current);switch(D){case 1:D=X.createElementNS("http://www.w3.org/2000/svg",P);break;case 2:D=X.createElementNS("http://www.w3.org/1998/Math/MathML",P);break;default:switch(P){case"svg":D=X.createElementNS("http://www.w3.org/2000/svg",P);break;case"math":D=X.createElementNS("http://www.w3.org/1998/Math/MathML",P);break;case"script":D=X.createElement("div"),D.innerHTML=" - + +