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 @@ + + \ 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 @@ + + +
+ +public folder of your project+ We use privacy-respecting analytics to improve the Algorithm Visualizer experience. + No personal data is collected - only anonymous usage statistics. +
+ ) : ( ++ Help us understand which algorithms are most popular +
++ Allow us to collect optional feedback and ratings +
++ You need to consent to feedback collection to submit feedback. + Please check your privacy settings. +
+Thank you for your feedback!
+