From 4d8061616d3f66a7bdf2e8703221e594e621e36e Mon Sep 17 00:00:00 2001 From: 5t4l1n Date: Fri, 25 Jul 2025 23:58:20 +0530 Subject: [PATCH] harini --- backend/main.py | 4 +- backend/models/user.py | 30 + backend/mongo_service.py | 93 +- backend/routes/auth.py | 121 +- backend/routes/coding.py | 88 + backend/routes/courses.py | 43 + backend/routes/quizzes.py | 32 + backend/seed_courses.py | 72 + backend/utils/adaptive_engine.py | 8 + frontend/.gitignore | 27 + frontend/app/coding/[problemId]/page.tsx | 11 + frontend/app/coding/page.tsx | 5 + .../[courseId]/lesson/[lessonId]/page.tsx | 91 + frontend/app/courses/[courseId]/page.tsx | 105 + frontend/app/courses/page.tsx | 5 + frontend/app/dashboard/page.tsx | 5 + frontend/app/globals.css | 90 + frontend/app/layout.tsx | 36 + frontend/app/page.tsx | 92 + frontend/app/quizzes/[quizId]/page.tsx | 11 + frontend/app/quizzes/page.tsx | 5 + frontend/app/test/page.tsx | 5 + frontend/components.json | 21 + frontend/components/auth-buttons.tsx | 128 + frontend/components/code-editor.tsx | 53 + frontend/components/coding-problem-list.tsx | 131 + frontend/components/coding-problem-view.tsx | 213 + frontend/components/course-list.tsx | 124 + frontend/components/course-sidebar.tsx | 45 + frontend/components/dashboard-stats.tsx | 168 + frontend/components/feedback-panel.tsx | 78 + frontend/components/lesson-viewer.tsx | 162 + frontend/components/metamask-connect.tsx | 30 + frontend/components/progress-tracker.tsx | 22 + frontend/components/question-card.tsx | 64 + frontend/components/quiz-list.tsx | 138 + frontend/components/quiz-runner.tsx | 269 + frontend/components/solution-tabs.tsx | 47 + frontend/components/test-results-panel.tsx | 64 + frontend/components/testing-session.tsx | 203 + frontend/components/theme-provider.tsx | 7 + frontend/components/ui/accordion.tsx | 58 + frontend/components/ui/alert-dialog.tsx | 141 + frontend/components/ui/alert.tsx | 59 + frontend/components/ui/aspect-ratio.tsx | 7 + frontend/components/ui/avatar.tsx | 50 + frontend/components/ui/badge.tsx | 36 + frontend/components/ui/breadcrumb.tsx | 115 + frontend/components/ui/button.tsx | 56 + frontend/components/ui/calendar.tsx | 213 + frontend/components/ui/card.tsx | 79 + frontend/components/ui/carousel.tsx | 262 + frontend/components/ui/chart.tsx | 365 ++ frontend/components/ui/checkbox.tsx | 30 + frontend/components/ui/collapsible.tsx | 11 + frontend/components/ui/command.tsx | 153 + frontend/components/ui/context-menu.tsx | 200 + frontend/components/ui/dialog.tsx | 122 + frontend/components/ui/drawer.tsx | 118 + frontend/components/ui/dropdown-menu.tsx | 200 + frontend/components/ui/form.tsx | 178 + frontend/components/ui/hover-card.tsx | 29 + frontend/components/ui/input-otp.tsx | 71 + frontend/components/ui/input.tsx | 22 + frontend/components/ui/label.tsx | 26 + frontend/components/ui/menubar.tsx | 236 + frontend/components/ui/navbar.tsx | 130 + frontend/components/ui/navigation-menu.tsx | 128 + frontend/components/ui/pagination.tsx | 117 + frontend/components/ui/popover.tsx | 31 + frontend/components/ui/progress.tsx | 28 + frontend/components/ui/radio-group.tsx | 44 + frontend/components/ui/resizable.tsx | 45 + frontend/components/ui/scroll-area.tsx | 48 + frontend/components/ui/select.tsx | 160 + frontend/components/ui/separator.tsx | 31 + frontend/components/ui/sheet.tsx | 140 + frontend/components/ui/sidebar.tsx | 763 +++ frontend/components/ui/skeleton.tsx | 15 + frontend/components/ui/slider.tsx | 28 + frontend/components/ui/sonner.tsx | 31 + frontend/components/ui/switch.tsx | 29 + frontend/components/ui/table.tsx | 117 + frontend/components/ui/tabs.tsx | 55 + frontend/components/ui/textarea.tsx | 22 + frontend/components/ui/toast.tsx | 129 + frontend/components/ui/toaster.tsx | 35 + frontend/components/ui/toggle-group.tsx | 61 + frontend/components/ui/toggle.tsx | 45 + frontend/components/ui/tooltip.tsx | 30 + frontend/components/ui/use-mobile.tsx | 19 + frontend/components/ui/use-toast.ts | 194 + frontend/context/auth-context.tsx | 203 + frontend/hooks/use-mobile.tsx | 19 + frontend/hooks/use-toast.ts | 194 + frontend/lib/api.ts | 25 + frontend/lib/firebase.ts | 21 + frontend/lib/types.ts | 150 + frontend/lib/utils.ts | 6 + frontend/next.config.mjs | 14 + frontend/package.json | 77 + frontend/pnpm-lock.yaml | 5313 +++++++++++++++++ frontend/postcss.config.mjs | 8 + frontend/public/placeholder-logo.png | Bin 0 -> 568 bytes frontend/public/placeholder-logo.svg | 1 + frontend/public/placeholder-user.jpg | Bin 0 -> 1635 bytes frontend/public/placeholder.jpg | Bin 0 -> 1064 bytes frontend/public/placeholder.svg | 1 + frontend/styles/globals.css | 90 + frontend/tailwind.config.ts | 89 + frontend/tsconfig.json | 27 + steps.md | 8 + 112 files changed, 14445 insertions(+), 59 deletions(-) create mode 100644 backend/models/user.py create mode 100644 backend/routes/coding.py create mode 100644 backend/routes/courses.py create mode 100644 backend/routes/quizzes.py create mode 100644 backend/seed_courses.py create mode 100644 backend/utils/adaptive_engine.py create mode 100644 frontend/.gitignore create mode 100644 frontend/app/coding/[problemId]/page.tsx create mode 100644 frontend/app/coding/page.tsx create mode 100644 frontend/app/courses/[courseId]/lesson/[lessonId]/page.tsx create mode 100644 frontend/app/courses/[courseId]/page.tsx create mode 100644 frontend/app/courses/page.tsx create mode 100644 frontend/app/dashboard/page.tsx create mode 100644 frontend/app/globals.css create mode 100644 frontend/app/layout.tsx create mode 100644 frontend/app/page.tsx create mode 100644 frontend/app/quizzes/[quizId]/page.tsx create mode 100644 frontend/app/quizzes/page.tsx create mode 100644 frontend/app/test/page.tsx create mode 100644 frontend/components.json create mode 100644 frontend/components/auth-buttons.tsx create mode 100644 frontend/components/code-editor.tsx create mode 100644 frontend/components/coding-problem-list.tsx create mode 100644 frontend/components/coding-problem-view.tsx create mode 100644 frontend/components/course-list.tsx create mode 100644 frontend/components/course-sidebar.tsx create mode 100644 frontend/components/dashboard-stats.tsx create mode 100644 frontend/components/feedback-panel.tsx create mode 100644 frontend/components/lesson-viewer.tsx create mode 100644 frontend/components/metamask-connect.tsx create mode 100644 frontend/components/progress-tracker.tsx create mode 100644 frontend/components/question-card.tsx create mode 100644 frontend/components/quiz-list.tsx create mode 100644 frontend/components/quiz-runner.tsx create mode 100644 frontend/components/solution-tabs.tsx create mode 100644 frontend/components/test-results-panel.tsx create mode 100644 frontend/components/testing-session.tsx create mode 100644 frontend/components/theme-provider.tsx create mode 100644 frontend/components/ui/accordion.tsx create mode 100644 frontend/components/ui/alert-dialog.tsx create mode 100644 frontend/components/ui/alert.tsx create mode 100644 frontend/components/ui/aspect-ratio.tsx create mode 100644 frontend/components/ui/avatar.tsx create mode 100644 frontend/components/ui/badge.tsx create mode 100644 frontend/components/ui/breadcrumb.tsx create mode 100644 frontend/components/ui/button.tsx create mode 100644 frontend/components/ui/calendar.tsx create mode 100644 frontend/components/ui/card.tsx create mode 100644 frontend/components/ui/carousel.tsx create mode 100644 frontend/components/ui/chart.tsx create mode 100644 frontend/components/ui/checkbox.tsx create mode 100644 frontend/components/ui/collapsible.tsx create mode 100644 frontend/components/ui/command.tsx create mode 100644 frontend/components/ui/context-menu.tsx create mode 100644 frontend/components/ui/dialog.tsx create mode 100644 frontend/components/ui/drawer.tsx create mode 100644 frontend/components/ui/dropdown-menu.tsx create mode 100644 frontend/components/ui/form.tsx create mode 100644 frontend/components/ui/hover-card.tsx create mode 100644 frontend/components/ui/input-otp.tsx create mode 100644 frontend/components/ui/input.tsx create mode 100644 frontend/components/ui/label.tsx create mode 100644 frontend/components/ui/menubar.tsx create mode 100644 frontend/components/ui/navbar.tsx create mode 100644 frontend/components/ui/navigation-menu.tsx create mode 100644 frontend/components/ui/pagination.tsx create mode 100644 frontend/components/ui/popover.tsx create mode 100644 frontend/components/ui/progress.tsx create mode 100644 frontend/components/ui/radio-group.tsx create mode 100644 frontend/components/ui/resizable.tsx create mode 100644 frontend/components/ui/scroll-area.tsx create mode 100644 frontend/components/ui/select.tsx create mode 100644 frontend/components/ui/separator.tsx create mode 100644 frontend/components/ui/sheet.tsx create mode 100644 frontend/components/ui/sidebar.tsx create mode 100644 frontend/components/ui/skeleton.tsx create mode 100644 frontend/components/ui/slider.tsx create mode 100644 frontend/components/ui/sonner.tsx create mode 100644 frontend/components/ui/switch.tsx create mode 100644 frontend/components/ui/table.tsx create mode 100644 frontend/components/ui/tabs.tsx create mode 100644 frontend/components/ui/textarea.tsx create mode 100644 frontend/components/ui/toast.tsx create mode 100644 frontend/components/ui/toaster.tsx create mode 100644 frontend/components/ui/toggle-group.tsx create mode 100644 frontend/components/ui/toggle.tsx create mode 100644 frontend/components/ui/tooltip.tsx create mode 100644 frontend/components/ui/use-mobile.tsx create mode 100644 frontend/components/ui/use-toast.ts create mode 100644 frontend/context/auth-context.tsx create mode 100644 frontend/hooks/use-mobile.tsx create mode 100644 frontend/hooks/use-toast.ts create mode 100644 frontend/lib/api.ts create mode 100644 frontend/lib/firebase.ts create mode 100644 frontend/lib/types.ts create mode 100644 frontend/lib/utils.ts create mode 100644 frontend/next.config.mjs create mode 100644 frontend/package.json create mode 100644 frontend/pnpm-lock.yaml create mode 100644 frontend/postcss.config.mjs create mode 100644 frontend/public/placeholder-logo.png create mode 100644 frontend/public/placeholder-logo.svg create mode 100644 frontend/public/placeholder-user.jpg create mode 100644 frontend/public/placeholder.jpg create mode 100644 frontend/public/placeholder.svg create mode 100644 frontend/styles/globals.css create mode 100644 frontend/tailwind.config.ts create mode 100644 frontend/tsconfig.json diff --git a/backend/main.py b/backend/main.py index 61402f4..82b7da3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -7,7 +7,7 @@ from mongo_service import MongoService from web3_service import Web3Service # Import all route blueprints -from routes import auth, test_flow, certificate, dashboard +from routes import auth, test_flow, certificate, dashboard , courses, quizzes load_dotenv() @@ -34,6 +34,8 @@ app.register_blueprint(auth.bp, url_prefix='/api/auth') app.register_blueprint(test_flow.bp, url_prefix='/api/test') app.register_blueprint(certificate.bp, url_prefix='/api/certificate') app.register_blueprint(dashboard.bp, url_prefix='/api/dashboard') +app.register_blueprint(courses.bp, url_prefix='/api/courses') +app.register_blueprint(quizzes.bp, url_prefix='/api/quizzes') @app.route('/') def health_check(): diff --git a/backend/models/user.py b/backend/models/user.py new file mode 100644 index 0000000..5093fb4 --- /dev/null +++ b/backend/models/user.py @@ -0,0 +1,30 @@ +from bson import ObjectId +from datetime import datetime +from pymongo.collection import Collection + +class UserModel: + def __init__(self, collection: Collection): + self.collection = collection + + async def get_by_wallet(self, wallet_address: str): + return await self.collection.find_one({"wallet_address": wallet_address.lower()}) + + async def create_user(self, wallet_address: str): + now = datetime.utcnow() + user = { + "wallet_address": wallet_address.lower(), + "created_at": now, + "last_login": now, + "total_tests": 0, + "certificates": [] + } + result = await self.collection.insert_one(user) + user["_id"] = result.inserted_id + return user + + async def update_last_login(self, wallet_address: str): + now = datetime.utcnow() + await self.collection.update_one( + {"wallet_address": wallet_address.lower()}, + {"$set": {"last_login": now}} + ) diff --git a/backend/mongo_service.py b/backend/mongo_service.py index 4c9aebe..55b27ee 100644 --- a/backend/mongo_service.py +++ b/backend/mongo_service.py @@ -3,8 +3,10 @@ from pymongo.errors import ServerSelectionTimeoutError from datetime import datetime, timedelta from typing import Dict, List, Optional, Any + class MongoService: def __init__(self, uri: str): + self.uri = uri # Store URI for sync operations try: # Simple connection without custom SSL context self.client = AsyncIOMotorClient( @@ -18,16 +20,15 @@ class MongoService: print(f"MongoDB connection failed: {e}") # Fallback to basic connection self.client = AsyncIOMotorClient(uri) - - self.db = self.client.openlearnx + self.db = self.client.openlearnx # Collections self.users = self.db.users self.questions = self.db.questions self.test_sessions = self.db.test_sessions self.certificates = self.db.certificates self.peer_reviews = self.db.peer_reviews - + async def init_db(self): """Initialize database with indexes and sample data""" try: @@ -38,13 +39,10 @@ class MongoService: # Create indexes await self.users.create_index("wallet_address", unique=True) await self.users.create_index("email", unique=True, sparse=True) - await self.questions.create_index("subject") await self.questions.create_index("difficulty") - await self.test_sessions.create_index("user_id") await self.test_sessions.create_index("created_at") - await self.certificates.create_index("user_id") await self.certificates.create_index("token_id", unique=True) @@ -60,4 +58,85 @@ class MongoService: print(f"Database initialization error: {e}") print("Continuing without database initialization...") - # ... rest of your existing methods remain the same + async def get_user_by_wallet(self, wallet_address: str): + """Get user by wallet address""" + return await self.users.find_one({"wallet_address": wallet_address.lower()}) + + async def create_user(self, wallet_address: str): + """Create a new user""" + now = datetime.utcnow() + user = { + "wallet_address": wallet_address.lower(), + "created_at": now, + "last_login": now, + "total_tests": 0, + "certificates": [] + } + result = await self.users.insert_one(user) + user["_id"] = result.inserted_id + return user + + async def update_user_login(self, wallet_address: str): + """Update user's last login time""" + await self.users.update_one( + {"wallet_address": wallet_address.lower()}, + {"$set": {"last_login": datetime.utcnow()}} + ) + + async def insert_sample_questions(self): + """Insert sample questions - implement based on your needs""" + # You'll need to implement this method based on your question structure + sample_questions = [ + { + "subject": "Python", + "difficulty": "beginner", + "question": "What is a variable in Python?", + "options": ["A storage location", "A function", "A loop", "A condition"], + "correct_answer": 0, + "created_at": datetime.utcnow() + }, + # Add more sample questions as needed + ] + await self.questions.insert_many(sample_questions) + + async def close_connection(self): + """Close the database connection""" + if self.client: + self.client.close() + print("MongoDB connection closed") + + def create_user_sync(self, wallet_address: str): + """Synchronous user creation using pymongo instead of motor""" + import pymongo + + # Create a synchronous connection for this operation only + client = pymongo.MongoClient(self.uri) + db = client.openlearnx + users = db.users + + try: + # Check if user exists + user = users.find_one({"wallet_address": wallet_address.lower()}) + + if not user: + # Create new user + new_user = { + "wallet_address": wallet_address.lower(), + "created_at": datetime.utcnow(), + "last_login": datetime.utcnow(), + "total_tests": 0, + "certificates": [] + } + result = users.insert_one(new_user) + new_user["_id"] = result.inserted_id + return new_user + else: + # Update last login + users.update_one( + {"wallet_address": wallet_address.lower()}, + {"$set": {"last_login": datetime.utcnow()}} + ) + return user + finally: + # Always close the connection + client.close() \ No newline at end of file diff --git a/backend/routes/auth.py b/backend/routes/auth.py index f58d3d0..ab51788 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -1,71 +1,90 @@ from flask import Blueprint, request, jsonify, current_app import jwt from datetime import datetime, timedelta +import secrets -bp = Blueprint('auth', __name__) +bp = Blueprint("auth", __name__) -@bp.route('/nonce', methods=['POST']) +# Store nonces temporarily (in production, use Redis or database) +nonces = {} + +@bp.route("/nonce", methods=["POST"]) def get_nonce(): - """Generate nonce for wallet signature""" data = request.get_json() - wallet_address = data.get('wallet_address') + wallet_address = data.get("wallet_address") if not wallet_address: - return jsonify({"error": "Wallet address required"}), 400 - - web3_service = current_app.config['WEB3_SERVICE'] - nonce = web3_service.generate_nonce() + return jsonify({"error": "wallet_address is required"}), 400 + # Generate nonce + nonce = secrets.token_hex(16) message = f"Sign this message to authenticate with OpenLearnX: {nonce}" - return jsonify({ - "nonce": nonce, - "message": message - }) + # Store nonce for this wallet address + nonces[wallet_address.lower()] = nonce + + return jsonify({"nonce": nonce, "message": message}) -@bp.route('/verify', methods=['POST']) -async def verify_signature(): - """Verify MetaMask signature and create session""" +@bp.route("/verify", methods=["POST"]) +def verify_signature(): data = request.get_json() - wallet_address = data.get('wallet_address') - signature = data.get('signature') - message = data.get('message') + wallet_address = data.get("wallet_address", "").lower() + signature = data.get("signature") + message = data.get("message") if not all([wallet_address, signature, message]): return jsonify({"error": "Missing required fields"}), 400 - web3_service = current_app.config['WEB3_SERVICE'] - mongo_service = current_app.config['MONGO_SERVICE'] + # Verify nonce + stored_nonce = nonces.get(wallet_address) + if not stored_nonce or stored_nonce not in message: + return jsonify({"error": "Invalid nonce"}), 400 - # Verify signature - if not web3_service.verify_signature(wallet_address, message, signature): - return jsonify({"error": "Invalid signature"}), 401 - - # Create or get user - user = await mongo_service.create_user(wallet_address) - await mongo_service.update_user_login(wallet_address) - - # Create JWT token - token_payload = { - 'user_id': str(user['_id']), - 'wallet_address': wallet_address, - 'exp': datetime.utcnow() + timedelta(days=7) - } - - token = jwt.encode( - token_payload, - current_app.config['SECRET_KEY'], - algorithm='HS256' - ) - - return jsonify({ - "success": True, - "token": token, - "user": { - "id": str(user['_id']), - "wallet_address": user['wallet_address'], - "created_at": user['created_at'].isoformat(), - "total_tests": user.get('total_tests', 0), - "certificates": len(user.get('certificates', [])) + try: + web3_service = current_app.config["WEB3_SERVICE"] + + # Verify signature + if not web3_service.verify_signature(wallet_address, message, signature): + return jsonify({"error": "Invalid signature"}), 401 + + # For now, create a mock user without database operations + # This bypasses the async MongoDB issues entirely + user = { + "_id": f"user_{wallet_address}", + "wallet_address": wallet_address, + "created_at": datetime.utcnow(), + "total_tests": 0, + "certificates": [] } - }) + + # Create JWT token + token_payload = { + "user_id": str(user["_id"]), + "wallet_address": wallet_address, + "exp": datetime.utcnow() + timedelta(days=7) + } + + token = jwt.encode( + token_payload, + current_app.config["SECRET_KEY"], + algorithm="HS256" + ) + + # Clean up nonce + if wallet_address in nonces: + del nonces[wallet_address] + + return jsonify({ + "success": True, + "token": token, + "user": { + "id": str(user["_id"]), + "wallet_address": user["wallet_address"], + "total_tests": user.get("total_tests", 0), + "certificates": len(user.get("certificates", [])) + } + }) + + except Exception as e: + print(f"Authentication error: {str(e)}") + return jsonify({"error": "Authentication failed"}), 500 diff --git a/backend/routes/coding.py b/backend/routes/coding.py new file mode 100644 index 0000000..97422b7 --- /dev/null +++ b/backend/routes/coding.py @@ -0,0 +1,88 @@ +from flask import Blueprint, jsonify, request, current_app +import requests +from bson import ObjectId +from datetime import datetime + +bp = Blueprint('coding', __name__) +PISTON_API_URL = "https://emkc.org/api/v2/piston/execute" + +@bp.route("/problems", methods=["GET"]) +async def get_problems(): + mongo = current_app.config['MONGO_SERVICE'] + problems = await mongo.db.coding_problems.find().to_list(100) + for p in problems: + p['_id'] = str(p['_id']) + return jsonify(problems) + +@bp.route("/problems/", methods=["GET"]) +async def get_problem(problem_id): + mongo = current_app.config['MONGO_SERVICE'] + prob = await mongo.db.coding_problems.find_one({"_id": ObjectId(problem_id)}) + if not prob: + return jsonify({"error": "Problem not found"}), 404 + prob['_id'] = str(prob['_id']) + return jsonify(prob) + +@bp.route("/run", methods=["POST"]) +async def run_code(): + data = request.json + problem_id = data.get("problem_id") + code = data.get("code") + language = data.get("language") + + mongo = current_app.config['MONGO_SERVICE'] + problem = await mongo.db.coding_problems.find_one({"_id": ObjectId(problem_id)}) + if not problem: + return jsonify({"error": "Problem not found"}), 404 + + # Concatenate all test case inputs + input_data = '\n'.join([tc['input'] for tc in problem['test_cases']]) + + try: + resp = requests.post( + PISTON_API_URL, + json={ + "language": language, + "source": code, + "input": input_data + }, + timeout=10, + ) + resp.raise_for_status() + result = resp.json() + except Exception as e: + return jsonify({"error": str(e)}), 500 + + # Compare output against expected (simple line-by-line check) + output_lines = result.get("output", "").strip().split('\n') + expected_outputs = [tc['expected_output'].strip() for tc in problem['test_cases']] + correct = output_lines == expected_outputs + + return jsonify({ + "output": result.get("output"), + "error": result.get("stderr"), + "runtime": result.get("stats", {}).get("duration"), + "correct": correct, + }) + +@bp.route("/submit", methods=["POST"]) +async def submit_solution(): + # Same as run_code, but can mark problem as solved + user = await get_authenticated_user() + if not user: + return jsonify({"error": "Unauthorized"}), 401 + + # Run the code first + result = await run_code() + jres = result.get_json() + + if jres.get("correct"): + mongo = current_app.config['MONGO_SERVICE'] + # Record that user solved problem + await mongo.db.user_solutions.update_one( + {"user_id": user['_id'], "problem_id": jres.get('problem_id')}, + {"$set": {"solved": True, "solved_at": datetime.utcnow()}}, + upsert=True + ) + return jsonify(jres) +`` diff --git a/backend/routes/courses.py b/backend/routes/courses.py new file mode 100644 index 0000000..345f6fe --- /dev/null +++ b/backend/routes/courses.py @@ -0,0 +1,43 @@ +from flask import Blueprint, jsonify, current_app +import asyncio +from bson import ObjectId + +bp = Blueprint('courses', __name__) + +# Remove trailing slash from route definition +@bp.route("/", methods=["GET"]) +@bp.route("", methods=["GET"]) # Add this line to handle both cases +def list_courses(): + try: + # Your existing course logic here + # Mock data for now since you're having DB async issues + courses = [ + { + "id": "python-course", + "title": "Python Programming Mastery", + "subject": "Programming", + "description": "Learn Python from basics to advanced concepts", + "difficulty": "Beginner to Advanced", + "progress": 0 + }, + { + "id": "java-course", + "title": "Java Development Bootcamp", + "subject": "Programming", + "description": "Master Java programming with object-oriented concepts", + "difficulty": "Intermediate", + "progress": 0 + }, + { + "id": "ethical-hacking-course", + "title": "Ethical Hacking & Cybersecurity", + "subject": "Cybersecurity", + "description": "Learn ethical hacking techniques and penetration testing", + "difficulty": "Advanced", + "progress": 0 + } + ] + return jsonify(courses) + except Exception as e: + print(f"Error in list_courses: {e}") + return jsonify({"error": "Failed to fetch courses"}), 500 diff --git a/backend/routes/quizzes.py b/backend/routes/quizzes.py new file mode 100644 index 0000000..1ca5289 --- /dev/null +++ b/backend/routes/quizzes.py @@ -0,0 +1,32 @@ +from flask import Blueprint, jsonify + +bp = Blueprint('quizzes', __name__) + +# Handle both with and without trailing slash +@bp.route("/", methods=["GET"]) +@bp.route("", methods=["GET"]) # Add this line +def list_quizzes(): + quizzes = [ + { + "id": "python-quiz", + "title": "Python Fundamentals Quiz", + "topic": "Programming", + "difficulty": "Easy", + "recent_performance": 85 + }, + { + "id": "java-quiz", + "title": "Java OOP Concepts Quiz", + "topic": "Programming", + "difficulty": "Medium", + "recent_performance": 78 + }, + { + "id": "security-quiz", + "title": "Cybersecurity Basics Quiz", + "topic": "Security", + "difficulty": "Hard", + "recent_performance": 72 + } + ] + return jsonify(quizzes) diff --git a/backend/seed_courses.py b/backend/seed_courses.py new file mode 100644 index 0000000..cd4668a --- /dev/null +++ b/backend/seed_courses.py @@ -0,0 +1,72 @@ +import asyncio +from mongo_service import MongoService +import os +from dotenv import load_dotenv + +load_dotenv() + +async def seed_courses(): + mongo_service = MongoService(os.getenv('MONGODB_URI')) + + courses = [ + { + "_id": "python-course", + "title": "Python Programming Mastery", + "subject": "Programming", + "description": "Learn Python from basics to advanced concepts including web development, data science, and automation.", + "difficulty": "Beginner to Advanced", + "modules": [ + { + "id": "python-basics", + "title": "Python Fundamentals", + "lessons": [ + {"id": "variables", "title": "Variables and Data Types", "type": "text"}, + {"id": "functions", "title": "Functions and Modules", "type": "code"} + ] + } + ] + }, + { + "_id": "java-course", + "title": "Java Development Bootcamp", + "subject": "Programming", + "description": "Master Java programming with object-oriented concepts, Spring framework, and enterprise development.", + "difficulty": "Intermediate", + "modules": [ + { + "id": "java-oop", + "title": "Object-Oriented Programming in Java", + "lessons": [ + {"id": "classes", "title": "Classes and Objects", "type": "code"}, + {"id": "inheritance", "title": "Inheritance and Polymorphism", "type": "text"} + ] + } + ] + }, + { + "_id": "ethical-hacking-course", + "title": "Ethical Hacking & Cybersecurity", + "subject": "Cybersecurity", + "description": "Learn ethical hacking techniques, penetration testing, and cybersecurity fundamentals to protect systems.", + "difficulty": "Advanced", + "modules": [ + { + "id": "recon", + "title": "Reconnaissance and Information Gathering", + "lessons": [ + {"id": "footprinting", "title": "Footprinting Techniques", "type": "text"}, + {"id": "scanning", "title": "Network Scanning", "type": "code"} + ] + } + ] + } + ] + + try: + await mongo_service.db.courses.insert_many(courses) + print("✅ Courses seeded successfully!") + except Exception as e: + print(f"❌ Error seeding courses: {e}") + +if __name__ == "__main__": + asyncio.run(seed_courses()) diff --git a/backend/utils/adaptive_engine.py b/backend/utils/adaptive_engine.py new file mode 100644 index 0000000..b84e680 --- /dev/null +++ b/backend/utils/adaptive_engine.py @@ -0,0 +1,8 @@ +def choose_next_question(current_difficulty: int, last_answer_correct: bool) -> int: + """ + Simplified adaptive engine logic adjusting difficulty for next question. + """ + if last_answer_correct: + return min(current_difficulty + 1, 3) # max difficulty = 3 + else: + return max(current_difficulty - 1, 1) # min difficulty = 1 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..f650315 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/frontend/app/coding/[problemId]/page.tsx b/frontend/app/coding/[problemId]/page.tsx new file mode 100644 index 0000000..651ae1b --- /dev/null +++ b/frontend/app/coding/[problemId]/page.tsx @@ -0,0 +1,11 @@ +import { CodingProblemView } from "@/components/coding-problem-view" + +interface CodingProblemPageProps { + params: { + problemId: string + } +} + +export default function CodingProblemPage({ params }: CodingProblemPageProps) { + return +} diff --git a/frontend/app/coding/page.tsx b/frontend/app/coding/page.tsx new file mode 100644 index 0000000..a80432e --- /dev/null +++ b/frontend/app/coding/page.tsx @@ -0,0 +1,5 @@ +import { CodingProblemList } from "@/components/coding-problem-list" + +export default function CodingPage() { + return +} diff --git a/frontend/app/courses/[courseId]/lesson/[lessonId]/page.tsx b/frontend/app/courses/[courseId]/lesson/[lessonId]/page.tsx new file mode 100644 index 0000000..02f5316 --- /dev/null +++ b/frontend/app/courses/[courseId]/lesson/[lessonId]/page.tsx @@ -0,0 +1,91 @@ +"use client" + +import { CourseSidebar } from "@/components/course-sidebar" +import { LessonViewer } from "@/components/lesson-viewer" +import { Loader2 } from "lucide-react" +import { useAuth } from "@/context/auth-context" +import { useRouter } from "next/navigation" +import { toast } from "react-hot-toast" +import { useState, useEffect } from "react" +import type { Course } from "@/lib/types" +import api from "@/lib/api" // Corrected import: default import + +interface CourseDetailPageProps { + params: { + courseId: string + lessonId: string + } +} + +export default function CourseDetailPage({ params }: CourseDetailPageProps) { + const { courseId, lessonId } = params + const { user, firebaseUser, isLoadingAuth } = useAuth() // Allow firebaseUser + const router = useRouter() + const [course, setCourse] = useState(null) + const [isLoadingCourse, setIsLoadingCourse] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + if (!isLoadingAuth && !user && !firebaseUser) { + // Allow either MetaMask or Firebase user + toast.error("Please login to view courses.") + router.push("/") + return + } + + const fetchCourse = async () => { + setIsLoadingCourse(true) + setError(null) + try { + // --- ORIGINAL API CALL (UNCOMMENT WHEN BACKEND IS READY) --- + const response = await api.get(`/api/courses/${courseId}`) + setCourse(response.data) + } catch (err: any) { + console.error("Failed to fetch course details:", err) + setError(err.response?.data?.message || "Failed to load course details.") + toast.error(err.response?.data?.message || "Failed to load course details.") + } finally { + setIsLoadingCourse(false) + } + } + + if (user || firebaseUser) { + // Only fetch if either user type is logged in + fetchCourse() + } + }, [user, firebaseUser, isLoadingAuth, router, courseId]) + + if (isLoadingAuth || isLoadingCourse) { + return ( +
+ + Loading course... +
+ ) + } + + if (error) { + return ( +
+

{error}

+
+ ) + } + + if (!course) { + return ( +
+

Course not found.

+
+ ) + } + + return ( +
+ +
+ +
+
+ ) +} diff --git a/frontend/app/courses/[courseId]/page.tsx b/frontend/app/courses/[courseId]/page.tsx new file mode 100644 index 0000000..9744bda --- /dev/null +++ b/frontend/app/courses/[courseId]/page.tsx @@ -0,0 +1,105 @@ +"use client" + +import { CourseSidebar } from "@/components/course-sidebar" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Loader2 } from "lucide-react" +import { useAuth } from "@/context/auth-context" +import { useRouter } from "next/navigation" +import { toast } from "react-hot-toast" +import { useState, useEffect } from "react" +import type { Course } from "@/lib/types" +import api from "@/lib/api" // Corrected import: default import + +interface CourseOverviewPageProps { + params: { + courseId: string + } +} + +export default function CourseOverviewPage({ params }: CourseOverviewPageProps) { + const { courseId } = params + const { user, firebaseUser, isLoadingAuth } = useAuth() // Allow firebaseUser + const router = useRouter() + const [course, setCourse] = useState(null) + const [isLoadingCourse, setIsLoadingCourse] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + if (!isLoadingAuth && !user && !firebaseUser) { + // Allow either MetaMask or Firebase user + toast.error("Please login to view courses.") + router.push("/") + return + } + + const fetchCourse = async () => { + setIsLoadingCourse(true) + setError(null) + try { + // --- ORIGINAL API CALL (UNCOMMENT WHEN BACKEND IS READY) --- + const response = await api.get(`/api/courses/${courseId}`) + setCourse(response.data) + } catch (err: any) { + console.error("Failed to fetch course details:", err) + setError(err.response?.data?.message || "Failed to load course details.") + toast.error(err.response?.data?.message || "Failed to load course details.") + } finally { + setIsLoadingCourse(false) + } + } + + if (user || firebaseUser) { + // Only fetch if either user type is logged in + fetchCourse() + } + }, [user, firebaseUser, isLoadingAuth, router, courseId]) + + useEffect(() => { + if (course && course.modules.length > 0 && course.modules[0].lessons.length > 0) { + // Redirect to the first lesson of the course + router.replace(`/courses/${courseId}/lesson/${course.modules[0].lessons[0].id}`) + } + }, [course, courseId, router]) + + if (isLoadingAuth || isLoadingCourse) { + return ( +
+ + Loading course... +
+ ) + } + + if (error) { + return ( +
+

{error}

+
+ ) + } + + if (!course) { + return ( +
+

Course not found.

+
+ ) + } + + return ( +
+ +
+ + + {course.title} Overview + + +

{course.description}

+

Select a lesson from the sidebar to begin.

+
+
+
+
+ ) +} diff --git a/frontend/app/courses/page.tsx b/frontend/app/courses/page.tsx new file mode 100644 index 0000000..5636d62 --- /dev/null +++ b/frontend/app/courses/page.tsx @@ -0,0 +1,5 @@ +import { CourseList } from "@/components/course-list" + +export default function CoursesPage() { + return +} diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx new file mode 100644 index 0000000..a060da9 --- /dev/null +++ b/frontend/app/dashboard/page.tsx @@ -0,0 +1,5 @@ +import { DashboardStatsOverview } from "@/components/dashboard-stats" + +export default function DashboardPage() { + return +} diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 0000000..df8e606 --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,90 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 0000000..f0fa3c7 --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,36 @@ +import type React from "react" +import type { Metadata } from "next" +import { Inter } from "next/font/google" +import "./globals.css" +import { Toaster } from "react-hot-toast" +import { AuthProvider } from "@/context/auth-context" +import { Navbar } from "@/components/ui/navbar" +import { ThemeProvider } from "@/components/theme-provider" + +const inter = Inter({ subsets: ["latin"] }) + +export const metadata: Metadata = { + title: "OpenLearnX - Decentralized Adaptive Learning", + description: "AI-powered adaptive testing with blockchain-secured credentials.", + generator: 'v0.dev' +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + + + +
{children}
+ +
+
+ + + ) +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 0000000..4c9e2e8 --- /dev/null +++ b/frontend/app/page.tsx @@ -0,0 +1,92 @@ +import Link from "next/link" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Brain, Zap, LinkIcon, BookOpen, Code, Lightbulb } from "lucide-react" + +export default function Home() { + return ( +
+
+
+
+

+ OpenLearnX – Decentralized Adaptive Learning +

+

+ Unlock your potential with AI-powered adaptive learning, coding practice, and blockchain-secured + credentials. +

+
+ + + +
+
+
+
+ +
+
+
+ + + + Interactive Courses + + + Engage with rich multimedia content, track your progress, and master new subjects at your own pace. + + + + + + LeetCode-Style Coding Practice + + + Sharpen your coding skills with interactive problems, instant feedback, and a built-in code editor. + + + + + + Advanced Quiz Platform + + + Test your knowledge with timed multiple-choice quizzes, detailed explanations, and performance tracking. + + + + + + AI-powered Adaptive Learning + + + Our platform intelligently adjusts content and question difficulty based on your performance. + + + + + + Blockchain-secured Credentials + + + Your achievements are secured on the blockchain, providing verifiable and tamper-proof records. + + + + + + Personalized Dashboard + + + Track your progress, identify strengths and weaknesses, and visualize your learning journey. + + +
+
+
+
+ ) +} diff --git a/frontend/app/quizzes/[quizId]/page.tsx b/frontend/app/quizzes/[quizId]/page.tsx new file mode 100644 index 0000000..ffe246b --- /dev/null +++ b/frontend/app/quizzes/[quizId]/page.tsx @@ -0,0 +1,11 @@ +import { QuizRunner } from "@/components/quiz-runner" + +interface QuizPageProps { + params: { + quizId: string + } +} + +export default function QuizPage({ params }: QuizPageProps) { + return +} diff --git a/frontend/app/quizzes/page.tsx b/frontend/app/quizzes/page.tsx new file mode 100644 index 0000000..c11d96e --- /dev/null +++ b/frontend/app/quizzes/page.tsx @@ -0,0 +1,5 @@ +import { QuizList } from "@/components/quiz-list" + +export default function QuizzesPage() { + return +} diff --git a/frontend/app/test/page.tsx b/frontend/app/test/page.tsx new file mode 100644 index 0000000..55b5f58 --- /dev/null +++ b/frontend/app/test/page.tsx @@ -0,0 +1,5 @@ +import { TestingSession } from "@/components/testing-session" + +export default function TestPage() { + return +} diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..d9ef0ae --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/frontend/components/auth-buttons.tsx b/frontend/components/auth-buttons.tsx new file mode 100644 index 0000000..a83351a --- /dev/null +++ b/frontend/components/auth-buttons.tsx @@ -0,0 +1,128 @@ +"use client" + +import { useState } from "react" +import { useAuth } from "@/context/auth-context" +import { Button } from "@/components/ui/button" +import { Loader2 } from "lucide-react" +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" + +export function AuthButtons() { + const { user, firebaseUser, isLoadingAuth, authMethod, connectWallet, loginWithEmail, signupWithEmail, logout } = + useAuth() + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const [isAuthModalOpen, setIsAuthModalOpen] = useState(false) + + const handleEmailLogin = async () => { + await loginWithEmail(email, password) + setIsAuthModalOpen(false) + } + + const handleEmailSignup = async () => { + await signupWithEmail(email, password) + setIsAuthModalOpen(false) + } + + const displayAddress = user?.wallet_address || firebaseUser?.email || "Guest" + + return ( +
+ {authMethod ? ( + <> + + Connected:{" "} + {authMethod === "metamask" && user?.wallet_address + ? `${user.wallet_address.slice(0, 6)}...${user.wallet_address.slice(-4)}` + : authMethod === "firebase" && firebaseUser?.email + ? firebaseUser.email + : displayAddress} + + + + ) : ( + + + + + + + Choose your authentication method + + + + MetaMask + Email + + +

+ Connect your MetaMask wallet for full access to courses, coding, and blockchain features. +

+ +
+ +

+ Use email for a quick testing process (quizzes only). +

+
+ + setEmail(e.target.value)} + className="dark:bg-gray-700 dark:border-gray-600" + /> +
+
+ + setPassword(e.target.value)} + className="dark:bg-gray-700 dark:border-gray-600" + /> +
+
+ + +
+
+
+
+
+ )} +
+ ) +} diff --git a/frontend/components/code-editor.tsx b/frontend/components/code-editor.tsx new file mode 100644 index 0000000..3b1df9e --- /dev/null +++ b/frontend/components/code-editor.tsx @@ -0,0 +1,53 @@ +"use client" + +import { Textarea } from "@/components/ui/textarea" +import { Label } from "@/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" + +interface CodeEditorProps { + code: string + onCodeChange: (code: string) => void + language: string + onLanguageChange: (lang: string) => void + availableLanguages: string[] + readOnly?: boolean +} + +export function CodeEditor({ + code, + onCodeChange, + language, + onLanguageChange, + availableLanguages, + readOnly = false, +}: CodeEditorProps) { + return ( +
+
+ + +
+