diff --git a/backend/app/database/__init__.py b/backend/app/database/__init__.py new file mode 100644 index 0000000..26abed1 --- /dev/null +++ b/backend/app/database/__init__.py @@ -0,0 +1,18 @@ +from motor.motor_asyncio import AsyncIOMotorClient +from app.database.analytics_schema import AnalyticsDB +import os + +# Get MongoDB connection string from environment or use default +MONGODB_URL = os.getenv("MONGODB_URL", "mongodb://localhost:27017/") +DB_NAME = os.getenv("MONGODB_DB_NAME", "algorithm_visualizer") + +# Create a global client instance +client = AsyncIOMotorClient(MONGODB_URL) +db = client[DB_NAME] + +# Initialize collections +analytics_db = AnalyticsDB(db) + +# Dependency to get database instance +async def get_database(): + return db diff --git a/backend/app/database/analytics_schema.py b/backend/app/database/analytics_schema.py new file mode 100644 index 0000000..cbed01c --- /dev/null +++ b/backend/app/database/analytics_schema.py @@ -0,0 +1,47 @@ +from pymongo import MongoClient, IndexModel, ASCENDING +from datetime import datetime, timedelta +import os + +class AnalyticsDB: + def __init__(self, db): + self.db = db + self.analytics_collection = db.analytics_events + self.feedback_collection = db.user_feedback + self.consent_collection = db.user_consent + self.setup_indexes() + + def setup_indexes(self): + # Create indexes for better query performance + analytics_indexes = [ + IndexModel([("timestamp", ASCENDING)]), + IndexModel([("event_type", ASCENDING)]), + IndexModel([("user_session", ASCENDING)]), + IndexModel([("algorithm", ASCENDING)]), + ] + + feedback_indexes = [ + IndexModel([("timestamp", ASCENDING)]), + IndexModel([("user_session", ASCENDING)]), + IndexModel([("algorithm", ASCENDING)]), + ] + + consent_indexes = [ + IndexModel([("user_session", ASCENDING)]), + IndexModel([("timestamp", ASCENDING)]), + ] + + self.analytics_collection.create_indexes(analytics_indexes) + self.feedback_collection.create_indexes(feedback_indexes) + self.consent_collection.create_indexes(consent_indexes) + + def clean_old_data(self, days_to_keep=90): + """Remove data older than specified days for GDPR compliance""" + cutoff_date = datetime.utcnow() - timedelta(days=days_to_keep) + + self.analytics_collection.delete_many({ + "timestamp": {"$lt": cutoff_date} + }) + + self.feedback_collection.delete_many({ + "timestamp": {"$lt": cutoff_date} + }) diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..8ea67f1 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,199 @@ +import os +from fastapi import FastAPI, Depends, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse +from pydantic import BaseModel, ConfigDict +from typing import List, Optional, Dict, Any +import uvicorn +from datetime import datetime, timedelta + +# Import database and analytics +from app.database import get_database, analytics_db +from app.routers import analytics as analytics_router + +app = FastAPI( + title="Algorithm Visualizer API", + description="Backend API for Algorithm Visualizer Platform with Analytics", + version="1.0.0" +) + +# Environment detection +ENVIRONMENT = os.getenv("ENVIRONMENT", "development") +IS_PRODUCTION = ENVIRONMENT == "production" + +# Configure CORS +origins = ["*"] if not IS_PRODUCTION else [ + "https://yourdomain.com", + "https://www.yourdomain.com" +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Include analytics router +app.include_router(analytics_router.router) + +# Lazy import services to avoid import issues +_sorting_service = None +_graph_service = None + +def get_sorting_service(): + global _sorting_service + if _sorting_service is None: + try: + from backend.services.sorting_service import SortingService + _sorting_service = SortingService() + except ImportError: + try: + from services.sorting_service import SortingService + _sorting_service = SortingService() + except ImportError as e: + raise HTTPException(status_code=500, detail=f"Cannot import SortingService: {e}") + return _sorting_service + +def get_graph_service(): + global _graph_service + if _graph_service is None: + try: + from backend.services.graph_service import GraphService + _graph_service = GraphService() + except ImportError: + try: + from services.graph_service import GraphService + _graph_service = GraphService() + except ImportError as e: + raise HTTPException(status_code=500, detail=f"Cannot import GraphService: {e}") + return _graph_service + +# Request models with Pydantic v2 config +class SortingRequest(BaseModel): + model_config = ConfigDict(populate_by_name=True) + array: List[int] + +class GraphNodeModel(BaseModel): + model_config = ConfigDict(populate_by_name=True) + id: int + label: Optional[str] = "" + x: Optional[float] = 0.0 + y: Optional[float] = 0.0 + +class GraphEdgeModel(BaseModel): + model_config = ConfigDict(populate_by_name=True) + from_node: int + to: int + weight: Optional[float] = 1.0 + directed: Optional[bool] = False + +class GraphRequest(BaseModel): + model_config = ConfigDict(populate_by_name=True) + nodes: List[GraphNodeModel] + edges: List[GraphEdgeModel] + start_node: Optional[int] = 0 + end_node: Optional[int] = None + +# Health check endpoint +@app.get("/api/health") +async def health_check(): + return {"status": "healthy", "environment": ENVIRONMENT} + +# Sorting endpoints +@app.post("/api/sort/{algorithm}") +async def run_sorting_algorithm(algorithm: str, request: SortingRequest): + service = get_sorting_service() + try: + result = service.sort(algorithm, request.array) + return {"sorted_array": result} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +# Graph endpoints +@app.post("/api/graph/{algorithm}") +async def run_graph_algorithm(algorithm: str, request: GraphRequest): + service = get_graph_service() + + # Convert request to the format expected by the service + graph_data = { + "nodes": [{"id": node.id, "label": node.label, "x": node.x, "y": node.y} + for node in request.nodes], + "edges": [{"from": edge.from_node, "to": edge.to, + "weight": edge.weight, "directed": edge.directed} + for edge in request.edges], + "start_node": request.start_node, + "end_node": request.end_node + } + + try: + result = service.run_algorithm(algorithm, graph_data) + return result + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +# String algorithms placeholder +@app.post("/api/string/{algorithm}") +async def run_string_algorithm(algorithm: str, request: dict): + return {"message": f"String algorithm {algorithm} execution not implemented yet"} + +# DP algorithms placeholder +@app.post("/api/dp/{algorithm}") +async def run_dp_algorithm(algorithm: str, request: dict): + return {"message": f"DP algorithm {algorithm} execution not implemented yet"} + +# Clean up old analytics data on startup +@app.on_event("startup") +async def startup_event(): + try: + # Clean up data older than 90 days + await analytics_db.clean_old_data(days_to_keep=90) + print("Successfully cleaned up old analytics data") + except Exception as e: + print(f"Error during startup cleanup: {e}") + +# Check if frontend is built and available +frontend_path = os.path.join(os.path.dirname(__file__), "..", "..", "frontend", "build") +frontend_available = os.path.exists(frontend_path) and os.path.isdir(frontend_path) + +if frontend_available: + # Serve static files from the frontend build directory + app.mount("/static", StaticFiles(directory=os.path.join(frontend_path, "static")), name="static") + + # Serve the React app for any other route + @app.get("/{full_path:path}") + async def serve_react_app(full_path: str): + # Don't interfere with API routes + if full_path.startswith("api/"): + return {"error": "Not found"}, 404 + + file_path = os.path.join(frontend_path, full_path) + if os.path.exists(file_path) and os.path.isfile(file_path): + return FileResponse(file_path) + + # Serve index.html for all other paths (client-side routing) + return FileResponse(os.path.join(frontend_path, "index.html")) + +if __name__ == "__main__": + port = int(os.environ.get("PORT", 8000)) + host = "0.0.0.0" if IS_PRODUCTION else "127.0.0.1" + + # Print startup information + print(f"Starting server in {ENVIRONMENT} mode") + print(f"Frontend available: {frontend_available}") + print(f"Server running at http://{host}:{port}") + + # Start the server + uvicorn.run( + "app.main:app", + host=host, + port=port, + reload=not IS_PRODUCTION, + workers=4 if IS_PRODUCTION else 1 + ) diff --git a/backend/app/models/analytics.py b/backend/app/models/analytics.py new file mode 100644 index 0000000..14c4c3f --- /dev/null +++ b/backend/app/models/analytics.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel, Field +from typing import Optional, Dict, Any +from datetime import datetime +from enum import Enum + +class EventType(str, Enum): + PAGE_VIEW = "page_view" + ALGORITHM_EXECUTION = "algorithm_execution" + SESSION_START = "session_start" + SESSION_END = "session_end" + FEEDBACK_SUBMITTED = "feedback_submitted" + THEME_CHANGED = "theme_changed" + SPEED_CHANGED = "speed_changed" + +class AnalyticsEvent(BaseModel): + event_type: EventType + algorithm: Optional[str] = None + input_size: Optional[int] = None + execution_time: Optional[float] = None + page_url: Optional[str] = None + user_session: str = Field(..., description="Anonymous session hash") + timestamp: datetime = Field(default_factory=datetime.utcnow) + metadata: Optional[Dict[str, Any]] = {} + +class UserFeedback(BaseModel): + user_session: str + rating: int = Field(..., ge=1, le=5) + feedback_text: Optional[str] = None + algorithm: Optional[str] = None + timestamp: datetime = Field(default_factory=datetime.utcnow) + +class ConsentSettings(BaseModel): + user_session: str + analytics_consent: bool = True + feedback_consent: bool = True + timestamp: datetime = Field(default_factory=datetime.utcnow) diff --git a/backend/app/routers/analytics.py b/backend/app/routers/analytics.py new file mode 100644 index 0000000..e151841 --- /dev/null +++ b/backend/app/routers/analytics.py @@ -0,0 +1,89 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from app.models.analytics import AnalyticsEvent, UserFeedback, ConsentSettings +from app.services.analytics_service import AnalyticsService +from app.database import get_database +from typing import Dict, Any + +router = APIRouter(prefix="/api/analytics", tags=["analytics"]) + +async def get_analytics_service(db = Depends(get_database)): + return AnalyticsService(db) + +@router.post("/event", status_code=status.HTTP_201_CREATED) +async def log_analytics_event( + event: AnalyticsEvent, + analytics_service: AnalyticsService = Depends(get_analytics_service) +): + """Log an analytics event""" + success = await analytics_service.log_event(event) + if success: + return {"message": "Event logged successfully"} + else: + return {"message": "Event not logged - no consent or error"} + +@router.post("/feedback", status_code=status.HTTP_201_CREATED) +async def submit_feedback( + feedback: UserFeedback, + analytics_service: AnalyticsService = Depends(get_analytics_service) +): + """Submit user feedback""" + success = await analytics_service.submit_feedback(feedback) + if success: + return {"message": "Feedback submitted successfully"} + else: + return {"message": "Feedback not submitted - no consent or error"} + +@router.post("/consent", status_code=status.HTTP_200_OK) +async def update_consent( + consent: ConsentSettings, + analytics_service: AnalyticsService = Depends(get_analytics_service) +): + """Update user consent settings""" + success = await analytics_service.update_consent(consent) + if success: + return {"message": "Consent updated successfully"} + else: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to update consent" + ) + +@router.get("/dashboard", response_model=Dict[str, Any]) +async def get_analytics_dashboard( + days: int = 30, + analytics_service: AnalyticsService = Depends(get_analytics_service) +): + """Get analytics dashboard data""" + try: + summary = await analytics_service.get_analytics_summary(days) + return summary + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to fetch analytics data: {str(e)}" + ) + +@router.delete("/data/{user_session}") +async def delete_user_data( + user_session: str, + analytics_service: AnalyticsService = Depends(get_analytics_service) +): + """Delete all data for a user session (GDPR compliance)""" + try: + # Delete from all collections + await analytics_service.analytics_db.analytics_collection.delete_many({ + "user_session": user_session + }) + await analytics_service.analytics_db.feedback_collection.delete_many({ + "user_session": user_session + }) + await analytics_service.analytics_db.consent_collection.delete_many({ + "user_session": user_session + }) + + return {"message": "User data deleted successfully"} + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to delete user data: {str(e)}" + ) diff --git a/backend/app/services/analytics_service.py b/backend/app/services/analytics_service.py new file mode 100644 index 0000000..9c90335 --- /dev/null +++ b/backend/app/services/analytics_service.py @@ -0,0 +1,127 @@ +from app.models.analytics import AnalyticsEvent, UserFeedback, ConsentSettings +from app.database.analytics_schema import AnalyticsDB +from datetime import datetime, timedelta +from typing import List, Dict, Any +import hashlib +import json + +class AnalyticsService: + def __init__(self, db): + self.analytics_db = AnalyticsDB(db) + + async def log_event(self, event: AnalyticsEvent) -> bool: + """Log analytics event if user has consented""" + try: + # Check user consent first + consent = await self.analytics_db.consent_collection.find_one({ + "user_session": event.user_session + }) + + if not consent or not consent.get("analytics_consent", False): + return False + + # Convert to dict and store + event_dict = event.dict() + event_dict["timestamp"] = event_dict["timestamp"].isoformat() + + await self.analytics_db.analytics_collection.insert_one(event_dict) + return True + except Exception as e: + print(f"Analytics logging error: {e}") + return False + + async def submit_feedback(self, feedback: UserFeedback) -> bool: + """Submit user feedback if consented""" + try: + # Check consent + consent = await self.analytics_db.consent_collection.find_one({ + "user_session": feedback.user_session + }) + + if not consent or not consent.get("feedback_consent", False): + return False + + feedback_dict = feedback.dict() + feedback_dict["timestamp"] = feedback_dict["timestamp"].isoformat() + + await self.analytics_db.feedback_collection.insert_one(feedback_dict) + return True + except Exception as e: + print(f"Feedback submission error: {e}") + return False + + async def update_consent(self, consent: ConsentSettings) -> bool: + """Update or create user consent settings""" + try: + consent_dict = consent.dict() + consent_dict["timestamp"] = consent_dict["timestamp"].isoformat() + + await self.analytics_db.consent_collection.update_one( + {"user_session": consent.user_session}, + {"$set": consent_dict}, + upsert=True + ) + return True + except Exception as e: + print(f"Consent update error: {e}") + return False + + async def get_analytics_summary(self, days: int = 30) -> Dict[str, Any]: + """Get analytics summary for dashboard""" + start_date = datetime.utcnow() - timedelta(days=days) + + pipeline = [ + {"$match": {"timestamp": {"$gte": start_date.isoformat()}}}, + {"$group": { + "_id": "$event_type", + "count": {"$sum": 1} + }} + ] + + event_counts = await self.analytics_db.analytics_collection.aggregate(pipeline).to_list(None) + + # Algorithm popularity + algorithm_pipeline = [ + {"$match": { + "event_type": "algorithm_execution", + "timestamp": {"$gte": start_date.isoformat()} + }}, + {"$group": { + "_id": "$algorithm", + "executions": {"$sum": 1}, + "avg_execution_time": {"$avg": "$execution_time"}, + "avg_input_size": {"$avg": "$input_size"} + }}, + {"$sort": {"executions": -1}}, + {"$limit": 10} + ] + + popular_algorithms = await self.analytics_db.analytics_collection.aggregate(algorithm_pipeline).to_list(None) + + # Page views + page_pipeline = [ + {"$match": { + "event_type": "page_view", + "timestamp": {"$gte": start_date.isoformat()} + }}, + {"$group": { + "_id": "$page_url", + "views": {"$sum": 1} + }}, + {"$sort": {"views": -1}} + ] + + page_views = await self.analytics_db.analytics_collection.aggregate(page_pipeline).to_list(None) + + # User sessions + total_sessions = await self.analytics_db.analytics_collection.distinct("user_session", { + "timestamp": {"$gte": start_date.isoformat()} + }) + + return { + "event_summary": event_counts, + "popular_algorithms": popular_algorithms, + "page_views": page_views, + "total_unique_sessions": len(total_sessions), + "period_days": days + } diff --git a/backend/requirements.txt b/backend/requirements.txt index f56bddb..46b4804 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -14,4 +14,7 @@ typing-extensions==4.12.2 python-json-logger==2.0.7 httpx==0.25.2 watchfiles==0.21.0 +motor==3.3.2 +pymongo==4.5.0 +python-dateutil==2.8.2 diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..30ef7c1 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,9 @@ +# API Configuration +VITE_API_BASE_URL=http://localhost:8000 + +# Analytics Configuration +VITE_ANALYTICS_ENABLED=true + +# App Configuration +VITE_APP_TITLE=Algorithm Visualizer Pro +VITE_NODE_ENV=development diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 662ddaf..5575542 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,6 +18,7 @@ "autoprefixer": "^10.4.14", "axios": "^1.3.4", "chart.js": "^4.2.1", + "crypto-js": "^4.2.0", "d3": "^7.8.2", "framer-motion": "^10.0.1", "lucide-react": "^0.323.0", @@ -31,7 +32,8 @@ "react-router-dom": "^6.8.1", "react-scripts": "5.0.1", "tailwindcss": "^3.2.7", - "use-sound": "^4.0.1" + "use-sound": "^4.0.1", + "uuid": "^13.0.0" }, "devDependencies": { "@types/react": "^18.0.28", @@ -98,7 +100,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -748,7 +749,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz", "integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, @@ -1632,7 +1632,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", @@ -5046,7 +5045,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -5171,7 +5169,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -5225,7 +5222,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -5595,7 +5591,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5694,7 +5689,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6660,7 +6654,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001735", "electron-to-chromium": "^1.5.204", @@ -6865,7 +6858,6 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", "license": "MIT", - "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -7323,6 +7315,12 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -7711,8 +7709,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/d3": { "version": "7.9.0", @@ -8031,7 +8028,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -8979,7 +8975,6 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -11862,7 +11857,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -15209,8 +15203,7 @@ "version": "0.36.1", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.36.1.tgz", "integrity": "sha512-/CaclMHKQ3A6rnzBzOADfwdSJ25BFoFT0Emxsc4zYVyav5SkK9iA6lEtIeuN/oRYbwPgviJT+t3l+sjFa28jYg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/ms": { "version": "2.1.3", @@ -15971,7 +15964,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -17159,7 +17151,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -17516,7 +17507,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -17677,7 +17667,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -17729,7 +17718,6 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -18238,7 +18226,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -18490,7 +18477,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -18890,6 +18876,15 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -19744,7 +19739,6 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -20125,7 +20119,6 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "license": "(MIT OR CC0-1.0)", - "peer": true, "engines": { "node": ">=10" }, @@ -20234,7 +20227,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -20457,12 +20449,16 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/v8-to-istanbul": { @@ -20561,7 +20557,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -20633,7 +20628,6 @@ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "license": "MIT", - "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -21046,7 +21040,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/frontend/package.json b/frontend/package.json index becc672..85e3d64 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "autoprefixer": "^10.4.14", "axios": "^1.3.4", "chart.js": "^4.2.1", + "crypto-js": "^4.2.0", "d3": "^7.8.2", "framer-motion": "^10.0.1", "lucide-react": "^0.323.0", @@ -26,7 +27,8 @@ "react-router-dom": "^6.8.1", "react-scripts": "5.0.1", "tailwindcss": "^3.2.7", - "use-sound": "^4.0.1" + "use-sound": "^4.0.1", + "uuid": "^13.0.0" }, "scripts": { "start": "react-scripts start", diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..aaa4046 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,5 @@ + + + + AV + \ No newline at end of file diff --git a/frontend/public/generate-assets.html b/frontend/public/generate-assets.html new file mode 100644 index 0000000..0ef04f1 --- /dev/null +++ b/frontend/public/generate-assets.html @@ -0,0 +1,146 @@ + + + + + Generate Assets + + + +

Asset Generator

+ +
+

Instructions:

+
    +
  1. Click each button below to generate the corresponding asset
  2. +
  3. Right-click on each canvas and select "Save image as..."
  4. +
  5. Save the files to the public folder of your project
  6. +
+
+ +
+

1. favicon.ico (64x64)

+ + +
+ +
+

2. logo192.png (192x192)

+ + +
+ +
+

3. logo512.png (512x512)

+ + +
+ + + + diff --git a/frontend/public/index.html b/frontend/public/index.html index c2a4bf6..3e4dbf2 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -2,16 +2,25 @@ - - - - - + + + + + + + + + + + - Algorithm Visualizer Platform + + + + + + + Algorithm Visualizer Pro @@ -21,6 +30,17 @@ + + + + + + + + + + + diff --git a/frontend/public/logo192.svg b/frontend/public/logo192.svg new file mode 100644 index 0000000..50ee96c --- /dev/null +++ b/frontend/public/logo192.svg @@ -0,0 +1,5 @@ + + + + AV + \ No newline at end of file diff --git a/frontend/public/logo512.svg b/frontend/public/logo512.svg new file mode 100644 index 0000000..eb6c9aa --- /dev/null +++ b/frontend/public/logo512.svg @@ -0,0 +1,5 @@ + + + + AV + \ No newline at end of file diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json index db79ff3..6c8cb48 100644 --- a/frontend/public/manifest.json +++ b/frontend/public/manifest.json @@ -1,21 +1,29 @@ { - "short_name": "AlgoViz Pro", + "short_name": "AlgoViz", "name": "Algorithm Visualizer Pro", + "description": "Interactive algorithm visualization platform", "icons": [ { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" + "src": "favicon.svg", + "type": "image/svg+xml", + "sizes": "any" }, { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" + "src": "logo192.svg", + "type": "image/svg+xml", + "sizes": "192x192", + "purpose": "any maskable" + }, + { + "src": "logo512.svg", + "type": "image/svg+xml", + "sizes": "512x512", + "purpose": "any maskable" } ], "start_url": ".", "display": "standalone", - "theme_color": "#3b82f6", + "theme_color": "#4F46E5", "background_color": "#ffffff", - "description": "Interactive platform for visualizing and learning algorithms" -} + "orientation": "portrait-primary" +} \ No newline at end of file diff --git a/frontend/public/sw.js b/frontend/public/sw.js new file mode 100644 index 0000000..95eddbf --- /dev/null +++ b/frontend/public/sw.js @@ -0,0 +1,64 @@ +// Service Worker for Algorithm Visualizer Pro +const CACHE_NAME = 'algo-viz-v1'; +const FALLBACK_ICON = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMTAwIDEwMCI+PHJlY3Qgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9IiM0RjQ2RTUiLz48dGV4dCB4PSI1MCIgeT0iNjAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IndoaXRlIiBmb250LWZhbWlseT0iQXJpYWwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyNCIgZm9udC13ZWlnaHQ9ImJvbGQiPkFWPC90ZXh0Pjwvc3ZnPg=='; + +// Install event - cache essential assets +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + return cache.addAll([ + '/', + '/index.html', + '/favicon.svg', + '/logo192.svg', + '/logo512.svg', + '/manifest.json' + ]); + }) + ); +}); + +// Fetch event - serve from cache, falling back to network +self.addEventListener('fetch', (event) => { + // Handle missing icon requests + if (event.request.url.endsWith('.ico') || + event.request.url.includes('logo') || + event.request.url.includes('favicon')) { + + event.respondWith( + fetch(event.request).catch(() => { + // Return fallback icon if asset is missing + return new Response(FALLBACK_ICON, { + headers: { + 'Content-Type': 'image/svg+xml', + 'Cache-Control': 'public, max-age=86400' + } + }); + }) + ); + return; + } + + // For other assets, try cache first, then network + event.respondWith( + caches.match(event.request).then((response) => { + return response || fetch(event.request); + }) + ); +}); + +// Activate event - clean up old caches +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME) { + return caches.delete(cacheName); + } + return null; + }).filter(Boolean) + ); + }) + ); +}); diff --git a/frontend/scripts/create-assets.js b/frontend/scripts/create-assets.js new file mode 100644 index 0000000..7f99ea9 --- /dev/null +++ b/frontend/scripts/create-assets.js @@ -0,0 +1,86 @@ +const fs = require('fs'); +const path = require('path'); + +// Create simple SVG-based assets +function createSVGIcon(size, filename, text) { + const svg = ` + + + ${text} +`; + + const publicDir = path.join(__dirname, '../public'); + const filePath = path.join(publicDir, filename); + + if (!fs.existsSync(publicDir)) { + fs.mkdirSync(publicDir, { recursive: true }); + } + + fs.writeFileSync(filePath, svg); + console.log(`āœ… Created ${filename} (${size}x${size})`); +} + +// Create favicon.ico +function createFavicon() { + const svg = ` + + + AV +`; + + const publicDir = path.join(__dirname, '../public'); + const filePath = path.join(publicDir, 'favicon.svg'); + + fs.writeFileSync(filePath, svg); + console.log('āœ… Created favicon.svg'); +} + +// Create all assets +console.log('šŸŽØ Creating placeholder assets...\n'); + +createFavicon(); +createSVGIcon(192, 'logo192.svg', 'AV'); +createSVGIcon(512, 'logo512.svg', 'AV'); + +// Create manifest.json +const manifest = { + "short_name": "AlgoViz", + "name": "Algorithm Visualizer Pro", + "description": "Interactive algorithm visualization platform", + "icons": [ + { + "src": "favicon.svg", + "type": "image/svg+xml", + "sizes": "any" + }, + { + "src": "logo192.svg", + "type": "image/svg+xml", + "sizes": "192x192", + "purpose": "any maskable" + }, + { + "src": "logo512.svg", + "type": "image/svg+xml", + "sizes": "512x512", + "purpose": "any maskable" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#4F46E5", + "background_color": "#ffffff", + "orientation": "portrait-primary" +}; + +// Write manifest file +const publicDir = path.join(__dirname, '../public'); +fs.writeFileSync( + path.join(publicDir, 'manifest.json'), + JSON.stringify(manifest, null, 2) +); +console.log('āœ… Created manifest.json'); + +console.log('\nšŸŽ‰ All assets created successfully!'); +console.log('šŸ’” Note: For production, consider creating proper PNG/ICO files.'); +console.log(' The SVG assets will work for development purposes.'); diff --git a/frontend/scripts/create-manifest.js b/frontend/scripts/create-manifest.js new file mode 100644 index 0000000..3b096f7 --- /dev/null +++ b/frontend/scripts/create-manifest.js @@ -0,0 +1,45 @@ +const fs = require('fs'); +const path = require('path'); + +const publicDir = path.join(__dirname, '../public'); + +// Create manifest.json +const manifest = { + "short_name": "AlgoVisualizer", + "name": "Algorithm Visualizer Pro", + "description": "Interactive algorithm visualization platform", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#4F46E5", + "background_color": "#ffffff" +}; + +// Write manifest file +fs.writeFileSync( + path.join(publicDir, 'manifest.json'), + JSON.stringify(manifest, null, 2) +); + +console.log('āœ… Created manifest.json in public directory'); +console.log('\nNext steps:'); +console.log('1. Open public/generate-assets.html in your browser'); +console.log('2. Generate and save the required image assets'); +console.log('3. Place them in the public/ directory'); +console.log('4. Run the development server with: npm start'); diff --git a/frontend/scripts/generate-assets.js b/frontend/scripts/generate-assets.js new file mode 100644 index 0000000..e0ed92f --- /dev/null +++ b/frontend/scripts/generate-assets.js @@ -0,0 +1,129 @@ +const fs = require('fs'); +const path = require('path'); +const { createCanvas } = require('canvas'); + +// Create public directory if it doesn't exist +const publicDir = path.join(__dirname, '../public'); +if (!fs.existsSync(publicDir)) { + fs.mkdirSync(publicDir, { recursive: true }); +} + +// Function to create a simple image with text +function createImageWithText(text, width, height, bgColor, textColor, outputPath) { + const canvas = createCanvas(width, height); + const ctx = canvas.getContext('2d'); + + // Draw background + ctx.fillStyle = bgColor; + ctx.fillRect(0, 0, width, height); + + // Draw text + ctx.fillStyle = textColor; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + // Calculate font size based on canvas dimensions + const fontSize = Math.min(width, height) * 0.15; + ctx.font = `bold ${fontSize}px Arial`; + + // Split text into multiple lines if needed + const words = text.split(' '); + let line = ''; + const lines = []; + const maxWidth = width * 0.9; + + for (const word of words) { + const testLine = line ? `${line} ${word}` : word; + const metrics = ctx.measureText(testLine); + + if (metrics.width > maxWidth && line) { + lines.push(line); + line = word; + } else { + line = testLine; + } + } + if (line) lines.push(line); + + // Draw each line of text + const lineHeight = fontSize * 1.2; + const startY = (height - (lines.length - 1) * lineHeight) / 2; + + lines.forEach((line, i) => { + ctx.fillText(line, width / 2, startY + (i * lineHeight)); + }); + + // Save the image + const buffer = canvas.toBuffer('image/png'); + fs.writeFileSync(outputPath, buffer); + console.log(`Created: ${outputPath}`); +} + +// Create manifest.json +const manifest = { + "short_name": "AlgoVisualizer", + "name": "Algorithm Visualizer Pro", + "description": "Interactive algorithm visualization platform", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#4F46E5", + "background_color": "#ffffff", + "orientation": "portrait-primary" +}; + +// Write manifest file +fs.writeFileSync( + path.join(publicDir, 'manifest.json'), + JSON.stringify(manifest, null, 2) +); +console.log('Created: manifest.json'); + +// Create favicon +createImageWithText( + 'AV', + 64, + 64, + '#4F46E5', + '#FFFFFF', + path.join(publicDir, 'favicon.ico') +); + +// Create logo192.png +createImageWithText( + 'Algorithm Visualizer', + 192, + 192, + '#4F46E5', + '#FFFFFF', + path.join(publicDir, 'logo192.png') +); + +// Create logo512.png +createImageWithText( + 'Algorithm Visualizer Pro\nInteractive Learning Platform', + 512, + 512, + '#4F46E5', + '#FFFFFF', + path.join(publicDir, 'logo512.png') +); + +console.log('\nāœ… Asset generation complete!'); +console.log('You can now start the development server with: npm start'); diff --git a/frontend/setup-env.sh b/frontend/setup-env.sh new file mode 100755 index 0000000..d4b874f --- /dev/null +++ b/frontend/setup-env.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Create .env.local file with default values +cat > .env.local < { // Check system preference first @@ -62,51 +71,7 @@ function App() { }, [theme]); return ( -
- - - - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - - - - - {/* Toast notifications */} +
+ + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + +
); } +// Component to handle analytics integration +const AnalyticsIntegration = ({ children }) => { + const { showConsentBanner, giveConsent } = useAnalytics(); + usePageTracking(); + + return ( + <> + {children} + {showConsentBanner && ( + giveConsent(false, false)} + /> + )} + + + + ); +}; + +// Main App component +function App() { + return ( + + + + + + + + ); +} + export default App; diff --git a/frontend/src/components/AnalyticsErrorBoundary.jsx b/frontend/src/components/AnalyticsErrorBoundary.jsx new file mode 100644 index 0000000..af8e6b7 --- /dev/null +++ b/frontend/src/components/AnalyticsErrorBoundary.jsx @@ -0,0 +1,27 @@ +import React from 'react'; + +class AnalyticsErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error) { + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) { + console.warn('Analytics error caught:', error, errorInfo); + } + + render() { + if (this.state.hasError) { + // Silently fail and render children without analytics + return this.props.children; + } + + return this.props.children; + } +} + +export default AnalyticsErrorBoundary; diff --git a/frontend/src/components/ConsentBanner.jsx b/frontend/src/components/ConsentBanner.jsx new file mode 100644 index 0000000..e6ed85f --- /dev/null +++ b/frontend/src/components/ConsentBanner.jsx @@ -0,0 +1,146 @@ +import React, { useState } from 'react'; +import { X, Shield, BarChart3, MessageSquare } from 'lucide-react'; +import { useAnalytics } from '../contexts/AnalyticsContext'; + +const ConsentBanner = ({ onDismiss }) => { + const [showDetails, setShowDetails] = useState(false); + const [analyticsConsent, setAnalyticsConsent] = useState(true); + const [feedbackConsent, setFeedbackConsent] = useState(true); + const { giveConsent } = useAnalytics(); + + const handleAccept = () => { + giveConsent(analyticsConsent, feedbackConsent); + if (onDismiss) onDismiss(); + }; + + const handleDecline = () => { + giveConsent(false, false); + if (onDismiss) onDismiss(); + }; + + return ( +
+
+
+
+
+ +

+ Privacy & Analytics +

+
+ + {!showDetails ? ( +

+ We use privacy-respecting analytics to improve the Algorithm Visualizer experience. + No personal data is collected - only anonymous usage statistics. +

+ ) : ( +
+
+

+ What we collect: +

+
    +
  • • Algorithm usage statistics (which algorithms you run)
  • +
  • • Page views and navigation patterns
  • +
  • • Performance metrics (execution times)
  • +
  • • Theme preferences and settings
  • +
+
+ +
+

+ What we DON'T collect: +

+
    +
  • • Personal information (name, email, etc.)
  • +
  • • IP addresses or location data
  • +
  • • Browser fingerprints
  • +
  • • Cross-site tracking
  • +
+
+ +
+
+
+ +
+
Usage Analytics
+

+ Help us understand which algorithms are most popular +

+
+
+ +
+ +
+
+ +
+
Feedback Collection
+

+ Allow us to collect optional feedback and ratings +

+
+
+ +
+
+
+ )} + +
+ + + + + +
+
+ + +
+
+
+ ); +}; + +export default ConsentBanner; diff --git a/frontend/src/components/EnvCheck.jsx b/frontend/src/components/EnvCheck.jsx new file mode 100644 index 0000000..da3d0f4 --- /dev/null +++ b/frontend/src/components/EnvCheck.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { config } from '../utils/config'; + +const EnvCheck = () => { + // Only show in development + if (!config.isDevelopment) return null; + + return ( +
+
Development Environment
+
+
API: {config.apiBaseUrl}
+
Analytics: {config.analyticsEnabled ? 'ENABLED' : 'DISABLED'}
+
Mode: {config.isDevelopment ? 'DEVELOPMENT' : 'PRODUCTION'}
+
+
+ ); +}; + +export default EnvCheck; diff --git a/frontend/src/components/FeedbackModal.jsx b/frontend/src/components/FeedbackModal.jsx new file mode 100644 index 0000000..1357026 --- /dev/null +++ b/frontend/src/components/FeedbackModal.jsx @@ -0,0 +1,136 @@ +import React, { useState } from 'react'; +import { X, Star, Send } from 'lucide-react'; +import { useAnalytics } from '../contexts/AnalyticsContext'; + +const FeedbackModal = ({ isOpen, onClose, algorithm = null }) => { + const [rating, setRating] = useState(0); + const [feedback, setFeedback] = useState(''); + const [submitted, setSubmitted] = useState(false); + const { analytics, consentGiven } = useAnalytics(); + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (rating === 0) return; + + const success = await analytics.submitFeedback(rating, feedback, algorithm); + if (success) { + setSubmitted(true); + setTimeout(() => { + onClose(); + setSubmitted(false); + setRating(0); + setFeedback(''); + }, 2000); + } + }; + + if (!isOpen) return null; + + if (!consentGiven.feedback) { + return ( +
+
+
+

+ Feedback Not Available +

+ +
+

+ You need to consent to feedback collection to submit feedback. + Please check your privacy settings. +

+
+
+ ); + } + + return ( +
+
+
+

+ {algorithm ? `Feedback for ${algorithm}` : 'Platform Feedback'} +

+ +
+ + {submitted ? ( +
+
+ +
+

Thank you for your feedback!

+
+ ) : ( +
+
+ +
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+
+ +
+ +