diff --git a/backend/main.py b/backend/main.py index a0d53a7..1ee37ca 100644 --- a/backend/main.py +++ b/backend/main.py @@ -47,6 +47,16 @@ except ImportError: DASHBOARD_AVAILABLE = False print("⚠️ Dashboard routes not available") +# ✅ Public modules/lessons endpoints for course pages +try: + from routes.modules_public import bp as modules_public_bp + MODULES_PUBLIC_AVAILABLE = True + print("✅ Public modules routes available") +except ImportError: + modules_public_bp = None + MODULES_PUBLIC_AVAILABLE = False + print("⚠️ Public modules routes not available") + # ✅ CRITICAL: Import certificate blueprint try: from routes.certificate import bp as certificate_bp @@ -64,6 +74,7 @@ blueprints_to_register = [ ('certificate', '/api/certificate'), # ✅ Use blueprint version ('dashboard', '/api/dashboard'), ('courses', '/api/courses'), + ('modules_public', '/api/modules'), ('quizzes', '/api/quizzes'), ('admin', '/api/admin'), ('exam', '/api/exam'), diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 5025ad3..61ecdb0 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -202,8 +202,24 @@ def admin_dashboard(): """Get admin dashboard statistics""" try: total_courses = db.courses.count_documents({}) - total_lessons = db.lessons.count_documents({}) - total_modules = db.modules.count_documents({}) + + # ✅ FIXED: Count modules and lessons from embedded course structure + total_modules = 0 + total_lessons = 0 + courses = list(db.courses.find({}, {"modules": 1})) + for course in courses: + modules = course.get("modules", []) + total_modules += len(modules) + for module in modules: + lessons = module.get("lessons", []) + total_lessons += len(lessons) + + # Fallback to separate collections if they exist + if total_modules == 0: + total_modules = db.modules.count_documents({}) + if total_lessons == 0: + total_lessons = db.lessons.count_documents({}) + total_users = db.users.count_documents({}) total_logs = db.security_logs.count_documents({}) active_students = db.users.count_documents({ @@ -1159,14 +1175,47 @@ def get_admin_courses(): """Get all courses for admin management""" try: print("Fetching courses from database...") - courses = list(db.courses.find({}, {"_id": 0})) + courses = list(db.courses.find({})) print(f"Found {len(courses)} courses") + + def normalize_title(value): + return " ".join(str(value or "").lower().split()) + + def course_score(item): + score = 0 + for field in ["subject", "difficulty", "mentor", "description", "video_url", "embed_url"]: + if item.get(field): + score += 1 + if isinstance(item.get("modules"), list): + score += min(len(item.get("modules", [])), 5) + return score for course in courses: + if not course.get("id") and "_id" in course: + course["id"] = str(course["_id"]) + if "_id" in course: + del course["_id"] + if not course.get("title"): + course["title"] = course.get("name", "") + if not course.get("subject"): + course["subject"] = course.get("category", course.get("topic", "")) + if not course.get("difficulty"): + course["difficulty"] = course.get("level", "") + if not course.get("mentor"): + course["mentor"] = course.get("instructor", course.get("instructor_name", course.get("mentor_name", ""))) + if not course.get("description"): + course["description"] = course.get("summary", "") course["students"] = course.get("students", 0) course["status"] = "published" - - return jsonify(courses) + + deduped = {} + for course in courses: + key = normalize_title(course.get("title")) or course.get("id") + existing = deduped.get(key) + if not existing or course_score(course) > course_score(existing): + deduped[key] = course + + return jsonify(list(deduped.values())) except Exception as e: print(f"Error fetching courses: {str(e)}") return jsonify({"error": str(e)}), 500 @@ -1261,16 +1310,27 @@ def delete_course(course_id): try: print(f"Deleting course: {course_id}") + course = db.courses.find_one({"id": course_id}) + if not course: + try: + course = db.courses.find_one({"_id": ObjectId(course_id)}) + except Exception: + course = None + + if not course: + return jsonify({"error": "Course not found"}), 404 + + course_key = course.get("id") or str(course.get("_id")) + # Delete related lessons first - lesson_result = db.lessons.delete_many({"course_id": course_id}) + lesson_result = db.lessons.delete_many({"course_id": course_key}) print(f"Deleted {lesson_result.deleted_count} related lessons") # Delete related modules - module_result = db.modules.delete_many({"course_id": course_id}) + module_result = db.modules.delete_many({"course_id": course_key}) print(f"Deleted {module_result.deleted_count} related modules") - - # Delete the course - result = db.courses.delete_one({"id": course_id}) + + result = db.courses.delete_one({"_id": course.get("_id")}) if result.deleted_count == 0: return jsonify({"error": "Course not found"}), 404 @@ -1690,8 +1750,23 @@ def get_admin_stats(): """Get detailed admin statistics""" try: total_courses = db.courses.count_documents({}) - total_lessons = db.lessons.count_documents({}) - total_modules = db.modules.count_documents({}) + + # ✅ FIXED: Count modules and lessons from embedded course structure + total_modules = 0 + total_lessons = 0 + courses = list(db.courses.find({}, {"modules": 1})) + for course in courses: + modules = course.get("modules", []) + total_modules += len(modules) + for module in modules: + lessons = module.get("lessons", []) + total_lessons += len(lessons) + + # Fallback to separate collections if they exist + if total_modules == 0: + total_modules = db.modules.count_documents({}) + if total_lessons == 0: + total_lessons = db.lessons.count_documents({}) # Course statistics by subject pipeline = [ diff --git a/backend/routes/auth.py b/backend/routes/auth.py index 9a87e28..284f3a4 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -18,7 +18,7 @@ client = MongoClient(mongo_uri) db = client.openlearnx # JWT secret - must be set via environment variable -JWT_SECRET = os.getenv('JWT_SECRET') +JWT_SECRET = os.getenv('JWT_SECRET_KEY') or os.getenv('JWT_SECRET') if not JWT_SECRET: import warnings import tempfile diff --git a/backend/routes/certificate.py b/backend/routes/certificate.py index a6bfce8..fb9b254 100644 --- a/backend/routes/certificate.py +++ b/backend/routes/certificate.py @@ -807,9 +807,20 @@ def get_user_certificates(user_id): db = create_isolated_mongodb_connection() if db is None: return jsonify({"error": "Database connection failed"}), 500 - + + normalized_user_id = str(user_id).strip() + if normalized_user_id.startswith("0x"): + normalized_user_id = normalized_user_id.lower() + certificates = list(db.certificates.find( - {"user_id": user_id}, + { + "$or": [ + {"user_id": user_id}, + {"user_id": normalized_user_id}, + {"wallet_address": user_id}, + {"wallet_address": normalized_user_id}, + ] + }, {"_id": 0, "encrypted_wallet_id": 0} ).sort("created_at", -1)) diff --git a/backend/routes/courses.py b/backend/routes/courses.py index 66df7b5..63d79c0 100644 --- a/backend/routes/courses.py +++ b/backend/routes/courses.py @@ -1,5 +1,6 @@ from flask import Blueprint, jsonify, current_app, request from pymongo import MongoClient +from bson import ObjectId import os from datetime import datetime from activity_logger import log_user_activity, resolve_user_identity @@ -16,19 +17,29 @@ db = client.openlearnx def list_courses(): """Get all courses - DYNAMIC from database""" try: - courses = list(db.courses.find({}, {"_id": 0})) + courses = list(db.courses.find({})) course_list = [] for course in courses: + # Handle both old format (id field) and new format (_id field) + course_id = course.get("id") or str(course.get("_id", "")) + modules_count = len(course.get("modules", [])) + if modules_count == 0 and course_id: + modules_count = db.modules.count_documents({"course_id": course_id}) course_data = { - "id": course.get("id"), - "title": course.get("title"), - "subject": course.get("subject"), - "description": course.get("description"), - "difficulty": course.get("difficulty"), - "mentor": course.get("mentor"), - "video_url": course.get("video_url"), - "embed_url": course.get("embed_url"), + "id": course_id, + "title": course.get("title", ""), + "subject": course.get("subject", ""), + "description": course.get("description", ""), + "difficulty": course.get("difficulty", ""), + "mentor": course.get("mentor", course.get("instructor", "")), + "video_url": course.get("video_url", ""), + "embed_url": course.get("embed_url", ""), + "thumbnail": course.get("thumbnail", ""), + "instructor": course.get("instructor", ""), + "duration_hours": course.get("duration_hours", 0), + "level": course.get("level", ""), + "modules": modules_count, "progress": course.get("progress", 0) } course_list.append(course_data) @@ -42,26 +53,111 @@ def list_courses(): def get_course(course_id): """Get specific course details - DYNAMIC""" try: - course = db.courses.find_one({"id": course_id}, {"_id": 0}) + # Try multiple ways to find the course + course = None + + # First try as string _id (new UUID format) + course = db.courses.find_one({"_id": course_id}) + + # If not found, try as ObjectId (old format) + if not course: + try: + course = db.courses.find_one({"_id": ObjectId(course_id)}) + except: + pass + + # If still not found, try as 'id' field (legacy) + if not course: + course = db.courses.find_one({"id": course_id}) if not course: return jsonify({"error": "Course not found"}), 404 + # Convert ObjectId to string for JSON serialization + course_id_value = course.get("id") or str(course.get("_id")) + course["id"] = course_id_value + course["_id"] = str(course.get("_id")) + if not course.get("mentor"): + course["mentor"] = course.get("instructor", "") + return jsonify(course) except Exception as e: print(f"Error in get_course: {e}") return jsonify({"error": "Failed to fetch course"}), 500 +@bp.route("//modules", methods=["GET"]) +def get_course_modules(course_id): + """Get modules for a course from modules collection or embedded structure.""" + try: + modules = list(db.modules.find({"course_id": course_id}).sort("order", 1)) + if not modules: + course = db.courses.find_one({"id": course_id}) + if not course: + try: + course = db.courses.find_one({"_id": ObjectId(course_id)}) + except Exception: + course = None + if course: + embedded = course.get("modules", []) + for module in embedded: + module["course_id"] = course_id + return jsonify({"success": True, "modules": embedded}) + + for module in modules: + if "_id" in module: + module["id"] = str(module["_id"]) + del module["_id"] + + return jsonify({"success": True, "modules": modules}) + except Exception as e: + print(f"Error fetching modules: {e}") + return jsonify({"error": "Failed to fetch modules"}), 500 + +@bp.route("/modules//lessons", methods=["GET"]) +def get_public_module_lessons(module_id): + """Get lessons for a module for public course pages.""" + try: + lessons = list(db.lessons.find({"module_id": module_id}).sort("order", 1)) + for lesson in lessons: + if "_id" in lesson: + lesson["id"] = str(lesson["_id"]) + del lesson["_id"] + return jsonify({"success": True, "lessons": lessons}) + except Exception as e: + print(f"Error fetching lessons: {e}") + return jsonify({"error": "Failed to fetch lessons"}), 500 + @bp.route("//lessons/", methods=["GET"]) def get_lesson(course_id, lesson_id): """Get specific lesson content - DYNAMIC""" try: - lesson = db.lessons.find_one({"id": lesson_id, "course_id": course_id}, {"_id": 0}) + # Find course with either format + course = None + course = db.courses.find_one({"_id": course_id}) + if not course: + try: + course = db.courses.find_one({"_id": ObjectId(course_id)}) + except: + pass + if not course: + course = db.courses.find_one({"id": course_id}) - if not lesson: - return jsonify({"error": "Lesson not found"}), 404 + if not course: + return jsonify({"error": "Course not found"}), 404 - return jsonify(lesson) + # Search for lesson in embedded modules structure + for module in course.get("modules", []): + for lesson in module.get("lessons", []): + if lesson.get("lesson_id") == lesson_id or lesson.get("id") == lesson_id: + return jsonify(lesson) + + # Fallback: check lessons collection + lesson = db.lessons.find_one({"id": lesson_id, "course_id": course_id}) + if lesson: + lesson["_id"] = str(lesson.get("_id", "")) + return jsonify(lesson) + + return jsonify({"error": "Lesson not found"}), 404 except Exception as e: print(f"Error in get_lesson: {e}") return jsonify({"error": "Failed to fetch lesson"}), 500 @@ -74,9 +170,31 @@ def mark_lesson_complete(course_id, lesson_id): user_id = identity.get("user_id") if user_id: - course = db.courses.find_one({"id": course_id}, {"title": 1}) or {} - lesson = db.lessons.find_one({"id": lesson_id, "course_id": course_id}, {"title": 1}) or {} - + # Find course with new or old format + course = db.courses.find_one({"_id": course_id}, {"title": 1}) + if not course: + try: + course = db.courses.find_one({"_id": ObjectId(course_id)}, {"title": 1}) + except: + pass + if not course: + course = db.courses.find_one({"id": course_id}, {"title": 1}) + + course = course or {} + + # Find lesson title from embedded structure or lessons collection + lesson_title = lesson_id + course_full = None + if course: + course_full = db.courses.find_one({"_id": course.get("_id")}) + + if course_full: + for module in course_full.get("modules", []): + for lesson in module.get("lessons", []): + if lesson.get("lesson_id") == lesson_id or lesson.get("id") == lesson_id: + lesson_title = lesson.get("title", lesson_id) + break + db.user_courses.update_one( {"user_id": user_id, "course_id": course_id}, { @@ -97,7 +215,7 @@ def mark_lesson_complete(course_id, lesson_id): user_id, "course", "Lesson completed", - f"Completed lesson '{lesson.get('title', lesson_id)}' in course '{course.get('title', course_id)}'", + f"Completed lesson '{lesson_title}' in course '{course.get('title', course_id)}'", {"course_id": course_id, "lesson_id": lesson_id}, points_earned=10, ) @@ -170,6 +288,65 @@ def log_course_activity(course_id): except Exception as e: return jsonify({"error": str(e)}), 500 +@bp.route("//register", methods=["POST"]) +def register_course(course_id): + """Register/enroll a user in a course and log a dashboard notification.""" + try: + identity = resolve_user_identity(request, db) + user_id = identity.get("user_id") + if not user_id: + return jsonify({"success": False, "error": "Authentication required"}), 401 + + course = db.courses.find_one({"id": course_id}, {"title": 1}) + if not course: + try: + course = db.courses.find_one({"_id": ObjectId(course_id)}, {"title": 1}) + except Exception: + course = None + course_title = course.get("title") if course else course_id + + db.user_courses.update_one( + {"user_id": user_id, "course_id": course_id}, + { + "$set": { + "user_id": user_id, + "course_id": course_id, + "enrolled": True, + "enrolled_at": datetime.utcnow(), + "last_activity_at": datetime.utcnow(), + }, + "$setOnInsert": {"completed": False, "lessons_completed": []}, + }, + upsert=True, + ) + + log_user_activity( + db, + user_id, + "course", + "Course registered", + f"Registered for course '{course_title}'", + {"course_id": course_id}, + ) + + return jsonify({"success": True}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@bp.route("//registration", methods=["GET"]) +def get_course_registration(course_id): + """Check whether the current user is registered for a course.""" + try: + identity = resolve_user_identity(request, db) + user_id = identity.get("user_id") + if not user_id: + return jsonify({"success": False, "registered": False, "error": "Authentication required"}), 401 + + record = db.user_courses.find_one({"user_id": user_id, "course_id": course_id}) + return jsonify({"success": True, "registered": bool(record)}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + @bp.route("//progress", methods=["GET"]) def get_course_progress(course_id): """Get user's progress in a specific course""" diff --git a/backend/routes/dashboard.py b/backend/routes/dashboard.py index 84cf141..8eec67d 100644 --- a/backend/routes/dashboard.py +++ b/backend/routes/dashboard.py @@ -28,15 +28,31 @@ def verify_wallet_authentication(): # ✅ FIXED: Verify JWT signature using JWT_SECRET_KEY from flask import current_app jwt_secret = current_app.config.get('JWT_SECRET_KEY') or os.getenv('JWT_SECRET_KEY') + fallback_secret = os.getenv('JWT_SECRET') + decoded = None + if jwt_secret: - decoded = jwt.decode( - token, - jwt_secret, - algorithms=["HS256", "RS256"] - ) - else: - logger.error("JWT_SECRET_KEY not configured") - decoded = None + try: + decoded = jwt.decode( + token, + jwt_secret, + algorithms=["HS256", "RS256"], + ) + except Exception as e: + logger.warning(f"⚠️ JWT decode failed with JWT_SECRET_KEY: {e}") + + if decoded is None and fallback_secret: + try: + decoded = jwt.decode( + token, + fallback_secret, + algorithms=["HS256", "RS256"], + ) + except Exception as e: + logger.warning(f"⚠️ JWT decode failed with JWT_SECRET: {e}") + + if decoded is None: + logger.error("JWT secrets not configured or token invalid") if decoded: user_id = decoded.get('sub') or decoded.get('user_id') or decoded.get('uid') or decoded.get('wallet_address') @@ -140,13 +156,33 @@ def get_comprehensive_stats(): "wallet_address": wallet_address }) + identity_candidates = {str(user_id)} + if wallet_address: + identity_candidates.add(str(wallet_address).lower()) + + # Resolve user identity aliases to avoid missing data across auth methods. + user_doc = None + try: + maybe_oid = ObjectId(str(user_id)) + user_doc = db.users.find_one({"_id": maybe_oid}) + except Exception: + user_doc = db.users.find_one({"wallet_address": str(user_id).lower()}) or db.users.find_one({"email": str(user_id).lower()}) + + if user_doc: + if user_doc.get("_id"): + identity_candidates.add(str(user_doc.get("_id"))) + if user_doc.get("wallet_address"): + identity_candidates.add(str(user_doc.get("wallet_address")).lower()) + if user_doc.get("email"): + identity_candidates.add(str(user_doc.get("email")).lower()) + # ✅ FETCH ONLY REAL DATA FROM MONGODB - user_stats = db.user_stats.find_one({"user_id": user_id}) - courses = list(db.user_courses.find({"user_id": user_id})) - quizzes = list(db.user_quizzes.find({"user_id": user_id})) - coding_submissions = list(db.user_submissions.find({"user_id": user_id})) - blockchain_data = db.user_blockchain.find_one({"user_id": user_id}) - achievements = list(db.user_achievements.find({"user_id": user_id})) + user_stats = db.user_stats.find_one({"user_id": {"$in": list(identity_candidates)}}) + courses = list(db.user_courses.find({"user_id": {"$in": list(identity_candidates)}})) + quizzes = list(db.user_quizzes.find({"user_id": {"$in": list(identity_candidates)}})) + coding_submissions = list(db.user_submissions.find({"user_id": {"$in": list(identity_candidates)}})) + blockchain_data = db.user_blockchain.find_one({"user_id": {"$in": list(identity_candidates)}}) + achievements = list(db.user_achievements.find({"user_id": {"$in": list(identity_candidates)}})) # Convert ObjectIds to strings for JSON serialization for collection in [courses, quizzes, coding_submissions, achievements]: @@ -185,7 +221,8 @@ def get_comprehensive_stats(): # Real calculations (no fake data) total_xp = calculate_real_total_xp(courses, quizzes, coding_submissions, achievements) - courses_completed = len([c for c in courses if c.get('completed', False)]) + completed_courses = len([c for c in courses if c.get('completed', False)]) + courses_completed = len(courses) if courses else completed_courses coding_problems_solved = len(coding_submissions) quiz_accuracy = calculate_real_quiz_accuracy(quizzes) coding_streak = calculate_real_coding_streak(coding_submissions) @@ -197,6 +234,7 @@ def get_comprehensive_stats(): comprehensive_stats = { "total_xp": total_xp, "courses_completed": courses_completed, + "completed_courses": completed_courses, "coding_problems_solved": coding_problems_solved, "quiz_accuracy": quiz_accuracy, "streak_data": { @@ -206,7 +244,7 @@ def get_comprehensive_stats(): }, "total_courses": len(courses), "total_quizzes": len(quizzes), - "global_rank": calculate_real_global_rank(user_stats, user_id) if user_stats else 0, + "global_rank": calculate_real_global_rank(user_stats, user_id, total_xp), "weekly_activity": weekly_activity, "monthly_goals": { "target": user_stats.get('monthly_target', 0) if user_stats else 0, @@ -766,10 +804,10 @@ def get_empty_stats(wallet_address=None): def calculate_real_total_xp(courses, quizzes, submissions, achievements): """Calculate total XP from ONLY real MongoDB data""" - course_xp = sum([c.get('points', 0) for c in courses if c.get('completed', False)]) - quiz_xp = sum([q.get('points', 0) for q in quizzes]) - coding_xp = sum([s.get('points_earned', 0) for s in submissions]) - achievement_xp = sum([a.get('points', 0) for a in achievements]) + course_xp = sum([c.get('points', 0) or 0 for c in courses if c.get('completed', False)]) + quiz_xp = sum([q.get('points', q.get('score', 0) or 0) or 0 for q in quizzes]) + coding_xp = sum([s.get('points_earned', s.get('score', 0) or 0) or 0 for s in submissions]) + achievement_xp = sum([a.get('points', 0) or 0 for a in achievements]) total = course_xp + quiz_xp + coding_xp + achievement_xp logger.info(f"📊 Real XP calculation: courses={course_xp}, quizzes={quiz_xp}, coding={coding_xp}, achievements={achievement_xp}, total={total}") @@ -853,16 +891,17 @@ def calculate_real_quiz_accuracy(quizzes): logger.info(f"📊 Real quiz accuracy: {accuracy}% from {len(quizzes)} quizzes") return accuracy -def calculate_real_global_rank(user_stats, user_id): +def calculate_real_global_rank(user_stats, user_id, total_xp=None): """Calculate global rank from ONLY real MongoDB data""" - if not user_stats: - return 0 - - user_xp = user_stats.get('total_xp', 0) - + user_xp = None + if user_stats: + user_xp = user_stats.get('total_xp', 0) + if user_xp is None: + user_xp = total_xp or 0 + try: higher_ranked = db.user_stats.count_documents({"total_xp": {"$gt": user_xp}}) - rank = higher_ranked + 1 + rank = higher_ranked + 1 if user_xp > 0 else 0 logger.info(f"📊 Real global rank: {rank} (XP: {user_xp})") return rank except Exception as e: diff --git a/backend/routes/modules_public.py b/backend/routes/modules_public.py new file mode 100644 index 0000000..8fed082 --- /dev/null +++ b/backend/routes/modules_public.py @@ -0,0 +1,32 @@ +from flask import Blueprint, jsonify +from pymongo import MongoClient +from bson import ObjectId +import os + +bp = Blueprint("modules_public", __name__) + +mongo_uri = os.getenv("MONGODB_URI", "mongodb://localhost:27017/") +client = MongoClient(mongo_uri) +db = client.openlearnx + + +@bp.route("//lessons", methods=["GET"]) +def get_public_module_lessons(module_id): + """Public: get lessons for a module by module id.""" + try: + lessons = list(db.lessons.find({"module_id": module_id}).sort("order", 1)) + if not lessons: + try: + oid = ObjectId(module_id) + lessons = list(db.lessons.find({"module_id": oid}).sort("order", 1)) + except Exception: + lessons = [] + + for lesson in lessons: + if "_id" in lesson: + lesson["id"] = str(lesson["_id"]) + del lesson["_id"] + + return jsonify({"success": True, "lessons": lessons}) + except Exception as e: + return jsonify({"error": f"Failed to fetch lessons: {str(e)}"}), 500 diff --git a/backend/services/real_compiler_service.py b/backend/services/real_compiler_service.py index b8af580..7c295e7 100644 --- a/backend/services/real_compiler_service.py +++ b/backend/services/real_compiler_service.py @@ -229,7 +229,6 @@ class RealCompilerService: "compile", "__import__", "open", - "input", "globals", "locals", "vars", diff --git a/frontend/app/admin/courses/page.tsx b/frontend/app/admin/courses/page.tsx index f42c5f4..b296670 100644 --- a/frontend/app/admin/courses/page.tsx +++ b/frontend/app/admin/courses/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react" import { useRouter } from "next/navigation" -import { Edit, Plus, Trash2 } from "lucide-react" +import { Edit, Plus, Trash2, ListTree } from "lucide-react" type Course = { id: string @@ -15,6 +15,26 @@ type Course = { students: number } +type Module = { + id: string + course_id: string + title: string + description?: string + order: number +} + +type Lesson = { + id: string + module_id: string + course_id: string + title: string + description?: string + video_url?: string + order: number + duration?: string + type?: string +} + const API_BASE = "http://127.0.0.1:5000" export default function AdminCoursesPage() { @@ -24,6 +44,7 @@ export default function AdminCoursesPage() { const [loading, setLoading] = useState(true) const [showAdd, setShowAdd] = useState(false) const [editing, setEditing] = useState(null) + const [managing, setManaging] = useState(null) const getToken = () => localStorage.getItem("admin_token") const headers = () => { @@ -160,12 +181,19 @@ export default function AdminCoursesPage() { courses.map((course) => ( {course.title} - {course.subject} - {course.difficulty} - {course.mentor} + {course.subject?.trim() || "—"} + {course.difficulty?.trim() || "—"} + {course.mentor?.trim() || "—"} {Number(course.students || 0).toLocaleString()}
+
) } @@ -293,3 +328,326 @@ function CourseFormModal({ ) } + +function CourseContentModal({ + course, + onClose, +}: { + course: Course + onClose: () => void +}) { + const [modules, setModules] = useState([]) + const [lessonsByModule, setLessonsByModule] = useState>({}) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [moduleForm, setModuleForm] = useState({ + title: "", + description: "", + order: 1, + }) + const [lessonModuleId, setLessonModuleId] = useState(null) + const [lessonForm, setLessonForm] = useState({ + title: "", + description: "", + video_url: "", + order: 1, + duration: "", + type: "video", + }) + + const adminHeaders = () => { + const token = localStorage.getItem("admin_token") + return token + ? { "Content-Type": "application/json", Authorization: `Bearer ${token}` } + : { "Content-Type": "application/json" } + } + + const loadModules = async () => { + setLoading(true) + try { + const resp = await fetch(`${API_BASE}/api/admin/courses/${course.id}/modules`, { + headers: adminHeaders(), + }) + if (!resp.ok) { + setModules([]) + setLessonsByModule({}) + return + } + const data = await resp.json() + const list = Array.isArray(data?.modules) ? data.modules : Array.isArray(data) ? data : [] + list.sort((a: Module, b: Module) => (a.order || 0) - (b.order || 0)) + setModules(list) + + const lessonsMap: Record = {} + await Promise.all( + list.map(async (module: Module) => { + const lessonsResp = await fetch(`${API_BASE}/api/admin/modules/${module.id}/lessons`, { + headers: adminHeaders(), + }) + if (!lessonsResp.ok) { + lessonsMap[module.id] = [] + return + } + const lessonsData = await lessonsResp.json() + const lessonsList = Array.isArray(lessonsData?.lessons) + ? lessonsData.lessons + : Array.isArray(lessonsData) + ? lessonsData + : [] + lessonsList.sort((a: Lesson, b: Lesson) => (a.order || 0) - (b.order || 0)) + lessonsMap[module.id] = lessonsList + }) + ) + setLessonsByModule(lessonsMap) + setModuleForm((prev) => ({ ...prev, order: list.length + 1 })) + } finally { + setLoading(false) + } + } + + const createModule = async () => { + if (!moduleForm.title.trim()) return + setSaving(true) + try { + const resp = await fetch(`${API_BASE}/api/admin/courses/${course.id}/modules`, { + method: "POST", + headers: adminHeaders(), + body: JSON.stringify({ + title: moduleForm.title.trim(), + description: moduleForm.description.trim(), + order: Number(moduleForm.order) || 1, + }), + }) + if (!resp.ok) { + alert("Failed to create module") + return + } + setModuleForm({ title: "", description: "", order: moduleForm.order + 1 }) + await loadModules() + } finally { + setSaving(false) + } + } + + const deleteModule = async (moduleId: string) => { + if (!confirm("Delete this module and all its lessons?")) return + const resp = await fetch(`${API_BASE}/api/admin/modules/${moduleId}`, { + method: "DELETE", + headers: adminHeaders(), + }) + if (!resp.ok) { + alert("Failed to delete module") + return + } + await loadModules() + } + + const createLesson = async (moduleId: string) => { + if (!lessonForm.title.trim()) return + setSaving(true) + try { + const resp = await fetch(`${API_BASE}/api/admin/modules/${moduleId}/lessons`, { + method: "POST", + headers: adminHeaders(), + body: JSON.stringify({ + title: lessonForm.title.trim(), + description: lessonForm.description.trim(), + video_url: lessonForm.video_url.trim(), + order: Number(lessonForm.order) || 1, + duration: lessonForm.duration.trim() || undefined, + type: lessonForm.type.trim() || "video", + }), + }) + if (!resp.ok) { + alert("Failed to create lesson") + return + } + setLessonForm({ title: "", description: "", video_url: "", order: lessonForm.order + 1, duration: "", type: "video" }) + setLessonModuleId(null) + await loadModules() + } finally { + setSaving(false) + } + } + + const deleteLesson = async (lessonId: string) => { + if (!confirm("Delete this lesson?")) return + const resp = await fetch(`${API_BASE}/api/admin/lessons/${lessonId}`, { + method: "DELETE", + headers: adminHeaders(), + }) + if (!resp.ok) { + alert("Failed to delete lesson") + return + } + await loadModules() + } + + useEffect(() => { + loadModules() + }, [course.id]) + + return ( +
+
+
+
+

Manage Modules & Lessons

+

{course.title}

+
+ +
+ +
+
+

Add Module

+
+ setModuleForm({ ...moduleForm, title: e.target.value })} + className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-800" + /> +