mirror of
https://github.com/th30d4y/OpenLearnX.git
synced 2026-05-26 19:26:33 +00:00
course completed up to module lesson
This commit is contained in:
+313
-22
@@ -54,6 +54,14 @@ def admin_required(f):
|
|||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
def serialize_document(doc):
|
||||||
|
"""Convert MongoDB document to JSON-serializable format"""
|
||||||
|
if doc:
|
||||||
|
if '_id' in doc:
|
||||||
|
doc['_id'] = str(doc['_id'])
|
||||||
|
return doc
|
||||||
|
return None
|
||||||
|
|
||||||
def serialize_course(course):
|
def serialize_course(course):
|
||||||
"""Convert MongoDB document to JSON-serializable format"""
|
"""Convert MongoDB document to JSON-serializable format"""
|
||||||
if course:
|
if course:
|
||||||
@@ -100,11 +108,13 @@ def admin_dashboard():
|
|||||||
try:
|
try:
|
||||||
total_courses = db.courses.count_documents({})
|
total_courses = db.courses.count_documents({})
|
||||||
total_lessons = db.lessons.count_documents({})
|
total_lessons = db.lessons.count_documents({})
|
||||||
|
total_modules = db.modules.count_documents({})
|
||||||
active_students = db.users.count_documents({"status": "active"}) or 2341
|
active_students = db.users.count_documents({"status": "active"}) or 2341
|
||||||
|
|
||||||
stats = {
|
stats = {
|
||||||
"total_courses": total_courses,
|
"total_courses": total_courses,
|
||||||
"total_lessons": total_lessons,
|
"total_lessons": total_lessons,
|
||||||
|
"total_modules": total_modules,
|
||||||
"active_students": active_students,
|
"active_students": active_students,
|
||||||
"completion_rate": 78
|
"completion_rate": 78
|
||||||
}
|
}
|
||||||
@@ -137,7 +147,7 @@ def create_course():
|
|||||||
"""Create new course"""
|
"""Create new course"""
|
||||||
try:
|
try:
|
||||||
data = request.json
|
data = request.json
|
||||||
print(f"Creating course with data: {data}") # Debug log
|
print(f"Creating course with data: {data}")
|
||||||
|
|
||||||
course_id = data.get('id') or f"{data.get('title', '').lower().replace(' ', '-').replace('&', 'and')}-course"
|
course_id = data.get('id') or f"{data.get('title', '').lower().replace(' ', '-').replace('&', 'and')}-course"
|
||||||
|
|
||||||
@@ -176,10 +186,10 @@ def create_course():
|
|||||||
@bp.route("/courses/<course_id>", methods=["PUT"])
|
@bp.route("/courses/<course_id>", methods=["PUT"])
|
||||||
@admin_required
|
@admin_required
|
||||||
def update_course(course_id):
|
def update_course(course_id):
|
||||||
"""Update existing course - FIXED VERSION"""
|
"""Update existing course"""
|
||||||
try:
|
try:
|
||||||
data = request.json
|
data = request.json
|
||||||
print(f"Updating course {course_id} with data: {data}") # Debug log
|
print(f"Updating course {course_id} with data: {data}")
|
||||||
|
|
||||||
update_data = {
|
update_data = {
|
||||||
"title": data.get('title'),
|
"title": data.get('title'),
|
||||||
@@ -194,14 +204,14 @@ def update_course(course_id):
|
|||||||
|
|
||||||
# Remove None values
|
# Remove None values
|
||||||
update_data = {k: v for k, v in update_data.items() if v is not None}
|
update_data = {k: v for k, v in update_data.items() if v is not None}
|
||||||
print(f"Filtered update data: {update_data}") # Debug log
|
print(f"Filtered update data: {update_data}")
|
||||||
|
|
||||||
result = db.courses.update_one(
|
result = db.courses.update_one(
|
||||||
{"id": course_id},
|
{"id": course_id},
|
||||||
{"$set": update_data}
|
{"$set": update_data}
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"Update result: matched={result.matched_count}, modified={result.modified_count}") # Debug log
|
print(f"Update result: matched={result.matched_count}, modified={result.modified_count}")
|
||||||
|
|
||||||
if result.matched_count == 0:
|
if result.matched_count == 0:
|
||||||
return jsonify({"error": "Course not found"}), 404
|
return jsonify({"error": "Course not found"}), 404
|
||||||
@@ -217,54 +227,323 @@ def update_course(course_id):
|
|||||||
@bp.route("/courses/<course_id>", methods=["DELETE"])
|
@bp.route("/courses/<course_id>", methods=["DELETE"])
|
||||||
@admin_required
|
@admin_required
|
||||||
def delete_course(course_id):
|
def delete_course(course_id):
|
||||||
"""Delete course"""
|
"""Delete course and all related modules and lessons"""
|
||||||
try:
|
try:
|
||||||
print(f"Deleting course: {course_id}") # Debug log
|
print(f"Deleting course: {course_id}")
|
||||||
|
|
||||||
|
# Delete related lessons first
|
||||||
|
lesson_result = db.lessons.delete_many({"course_id": course_id})
|
||||||
|
print(f"Deleted {lesson_result.deleted_count} related lessons")
|
||||||
|
|
||||||
|
# Delete related modules
|
||||||
|
module_result = db.modules.delete_many({"course_id": course_id})
|
||||||
|
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_id})
|
||||||
|
|
||||||
if result.deleted_count == 0:
|
if result.deleted_count == 0:
|
||||||
return jsonify({"error": "Course not found"}), 404
|
return jsonify({"error": "Course not found"}), 404
|
||||||
|
|
||||||
# Also delete related lessons
|
|
||||||
lesson_result = db.lessons.delete_many({"course_id": course_id})
|
|
||||||
print(f"Deleted {lesson_result.deleted_count} related lessons") # Debug log
|
|
||||||
|
|
||||||
return jsonify({"success": True, "message": "Course deleted successfully"})
|
return jsonify({"success": True, "message": "Course deleted successfully"})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error deleting course: {e}")
|
print(f"Error deleting course: {e}")
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
# ✅ FIXED: Module Management Endpoints (removed duplicates)
|
||||||
|
@bp.route("/courses/<course_id>/modules", methods=["GET"])
|
||||||
|
@admin_required
|
||||||
|
def get_course_modules(course_id):
|
||||||
|
"""Get all modules for a specific course"""
|
||||||
|
try:
|
||||||
|
print(f"Fetching modules for course: {course_id}")
|
||||||
|
|
||||||
|
modules = list(db.modules.find({"course_id": course_id}).sort("order", 1))
|
||||||
|
|
||||||
|
# Convert ObjectId to string
|
||||||
|
for module in modules:
|
||||||
|
if '_id' in module:
|
||||||
|
module['id'] = str(module['_id'])
|
||||||
|
del module['_id']
|
||||||
|
|
||||||
|
print(f"Found {len(modules)} modules for course {course_id}")
|
||||||
|
return jsonify({"success": True, "modules": modules})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching modules: {str(e)}")
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
@bp.route("/courses/<course_id>/modules", methods=["POST"])
|
@bp.route("/courses/<course_id>/modules", methods=["POST"])
|
||||||
@admin_required
|
@admin_required
|
||||||
def add_module(course_id):
|
def create_module(course_id):
|
||||||
"""Add module to course"""
|
"""Create a new module for a course"""
|
||||||
try:
|
try:
|
||||||
data = request.json
|
data = request.json
|
||||||
|
print(f"Creating module for course {course_id} with data: {data}")
|
||||||
|
|
||||||
|
# Verify course exists
|
||||||
|
course = db.courses.find_one({"id": course_id})
|
||||||
|
if not course:
|
||||||
|
return jsonify({"error": "Course not found"}), 404
|
||||||
|
|
||||||
module = {
|
module = {
|
||||||
"id": data.get('id') or str(uuid.uuid4()),
|
"course_id": course_id,
|
||||||
"title": data.get('title'),
|
"title": data.get('title'),
|
||||||
"lessons": []
|
"description": data.get('description', ''),
|
||||||
|
"order": data.get('order', 1),
|
||||||
|
"created_at": datetime.now().isoformat(),
|
||||||
|
"updated_at": datetime.now().isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
result = db.courses.update_one(
|
result = db.modules.insert_one(module)
|
||||||
{"id": course_id},
|
module['id'] = str(result.inserted_id)
|
||||||
{"$push": {"modules": module}}
|
if '_id' in module:
|
||||||
|
del module['_id']
|
||||||
|
|
||||||
|
print(f"Module created with ID: {result.inserted_id}")
|
||||||
|
return jsonify({"success": True, "module": module}), 201
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating module: {str(e)}")
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@bp.route("/modules/<module_id>", methods=["GET"])
|
||||||
|
@admin_required
|
||||||
|
def get_module(module_id):
|
||||||
|
"""Get a specific module by ID"""
|
||||||
|
try:
|
||||||
|
print(f"Fetching module: {module_id}")
|
||||||
|
|
||||||
|
module = db.modules.find_one({"_id": ObjectId(module_id)})
|
||||||
|
|
||||||
|
if not module:
|
||||||
|
return jsonify({"error": "Module not found"}), 404
|
||||||
|
|
||||||
|
# Convert ObjectId to string
|
||||||
|
if '_id' in module:
|
||||||
|
module['id'] = str(module['_id'])
|
||||||
|
del module['_id']
|
||||||
|
|
||||||
|
return jsonify({"success": True, "module": module})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching module: {str(e)}")
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@bp.route("/modules/<module_id>", methods=["PUT"])
|
||||||
|
@admin_required
|
||||||
|
def update_module(module_id):
|
||||||
|
"""Update an existing module"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
print(f"Updating module {module_id} with data: {data}")
|
||||||
|
|
||||||
|
update_data = {
|
||||||
|
"title": data.get('title'),
|
||||||
|
"description": data.get('description'),
|
||||||
|
"order": data.get('order'),
|
||||||
|
"updated_at": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove None values
|
||||||
|
update_data = {k: v for k, v in update_data.items() if v is not None}
|
||||||
|
|
||||||
|
result = db.modules.update_one(
|
||||||
|
{"_id": ObjectId(module_id)},
|
||||||
|
{"$set": update_data}
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.matched_count == 0:
|
if result.matched_count == 0:
|
||||||
return jsonify({"error": "Course not found"}), 404
|
return jsonify({"error": "Module not found"}), 404
|
||||||
|
|
||||||
|
# Get updated module
|
||||||
|
updated_module = db.modules.find_one({"_id": ObjectId(module_id)})
|
||||||
|
if updated_module:
|
||||||
|
updated_module['id'] = str(updated_module['_id'])
|
||||||
|
del updated_module['_id']
|
||||||
|
|
||||||
|
return jsonify({"success": True, "module": updated_module})
|
||||||
|
|
||||||
return jsonify({"success": True, "module": module})
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
print(f"Error updating module: {str(e)}")
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@bp.route("/modules/<module_id>", methods=["DELETE"])
|
||||||
|
@admin_required
|
||||||
|
def delete_module(module_id):
|
||||||
|
"""Delete a module and all its lessons"""
|
||||||
|
try:
|
||||||
|
print(f"Deleting module: {module_id}")
|
||||||
|
|
||||||
|
# Delete related lessons first
|
||||||
|
lesson_result = db.lessons.delete_many({"module_id": module_id})
|
||||||
|
print(f"Deleted {lesson_result.deleted_count} related lessons")
|
||||||
|
|
||||||
|
# Delete the module
|
||||||
|
result = db.modules.delete_one({"_id": ObjectId(module_id)})
|
||||||
|
|
||||||
|
if result.deleted_count == 0:
|
||||||
|
return jsonify({"error": "Module not found"}), 404
|
||||||
|
|
||||||
|
return jsonify({"success": True, "message": "Module deleted successfully"})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error deleting module: {str(e)}")
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
# ✅ FIXED: Lesson Management Endpoints
|
||||||
|
@bp.route("/modules/<module_id>/lessons", methods=["GET"])
|
||||||
|
@admin_required
|
||||||
|
def get_module_lessons(module_id):
|
||||||
|
"""Get all lessons for a specific module"""
|
||||||
|
try:
|
||||||
|
print(f"Fetching lessons for module: {module_id}")
|
||||||
|
|
||||||
|
lessons = list(db.lessons.find({"module_id": module_id}).sort("order", 1))
|
||||||
|
|
||||||
|
# Convert ObjectId to string
|
||||||
|
for lesson in lessons:
|
||||||
|
if '_id' in lesson:
|
||||||
|
lesson['id'] = str(lesson['_id'])
|
||||||
|
del lesson['_id']
|
||||||
|
|
||||||
|
print(f"Found {len(lessons)} lessons for module {module_id}")
|
||||||
|
return jsonify({"success": True, "lessons": lessons})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching lessons: {str(e)}")
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@bp.route("/modules/<module_id>/lessons", methods=["POST"])
|
||||||
|
@admin_required
|
||||||
|
def create_lesson(module_id):
|
||||||
|
"""Create a new lesson for a module"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
print(f"Creating lesson for module {module_id} with data: {data}")
|
||||||
|
|
||||||
|
# Verify module exists
|
||||||
|
module = db.modules.find_one({"_id": ObjectId(module_id)})
|
||||||
|
if not module:
|
||||||
|
return jsonify({"error": "Module not found"}), 404
|
||||||
|
|
||||||
|
lesson = {
|
||||||
|
"module_id": module_id,
|
||||||
|
"course_id": module.get('course_id'),
|
||||||
|
"title": data.get('title'),
|
||||||
|
"description": data.get('description', ''),
|
||||||
|
"video_url": data.get('video_url'),
|
||||||
|
"embed_url": convert_to_embed_url(data.get('video_url')) if data.get('video_url') else None,
|
||||||
|
"order": data.get('order', 1),
|
||||||
|
"duration": data.get('duration'),
|
||||||
|
"type": data.get('type', 'video'),
|
||||||
|
"content": data.get('content', ''),
|
||||||
|
"created_at": datetime.now().isoformat(),
|
||||||
|
"updated_at": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
result = db.lessons.insert_one(lesson)
|
||||||
|
lesson['id'] = str(result.inserted_id)
|
||||||
|
if '_id' in lesson:
|
||||||
|
del lesson['_id']
|
||||||
|
|
||||||
|
print(f"Lesson created with ID: {result.inserted_id}")
|
||||||
|
return jsonify({"success": True, "lesson": lesson}), 201
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating lesson: {str(e)}")
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@bp.route("/lessons/<lesson_id>", methods=["GET"])
|
||||||
|
@admin_required
|
||||||
|
def get_lesson(lesson_id):
|
||||||
|
"""Get a specific lesson by ID"""
|
||||||
|
try:
|
||||||
|
print(f"Fetching lesson: {lesson_id}")
|
||||||
|
|
||||||
|
lesson = db.lessons.find_one({"_id": ObjectId(lesson_id)})
|
||||||
|
|
||||||
|
if not lesson:
|
||||||
|
return jsonify({"error": "Lesson not found"}), 404
|
||||||
|
|
||||||
|
# Convert ObjectId to string
|
||||||
|
if '_id' in lesson:
|
||||||
|
lesson['id'] = str(lesson['_id'])
|
||||||
|
del lesson['_id']
|
||||||
|
|
||||||
|
return jsonify({"success": True, "lesson": lesson})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching lesson: {str(e)}")
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@bp.route("/lessons/<lesson_id>", methods=["PUT"])
|
||||||
|
@admin_required
|
||||||
|
def update_lesson(lesson_id):
|
||||||
|
"""Update an existing lesson"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
print(f"Updating lesson {lesson_id} with data: {data}")
|
||||||
|
|
||||||
|
update_data = {
|
||||||
|
"title": data.get('title'),
|
||||||
|
"description": data.get('description'),
|
||||||
|
"video_url": data.get('video_url'),
|
||||||
|
"embed_url": convert_to_embed_url(data.get('video_url')) if data.get('video_url') else None,
|
||||||
|
"order": data.get('order'),
|
||||||
|
"duration": data.get('duration'),
|
||||||
|
"type": data.get('type'),
|
||||||
|
"content": data.get('content'),
|
||||||
|
"updated_at": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove None values
|
||||||
|
update_data = {k: v for k, v in update_data.items() if v is not None}
|
||||||
|
|
||||||
|
result = db.lessons.update_one(
|
||||||
|
{"_id": ObjectId(lesson_id)},
|
||||||
|
{"$set": update_data}
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.matched_count == 0:
|
||||||
|
return jsonify({"error": "Lesson not found"}), 404
|
||||||
|
|
||||||
|
# Get updated lesson
|
||||||
|
updated_lesson = db.lessons.find_one({"_id": ObjectId(lesson_id)})
|
||||||
|
if updated_lesson:
|
||||||
|
updated_lesson['id'] = str(updated_lesson['_id'])
|
||||||
|
del updated_lesson['_id']
|
||||||
|
|
||||||
|
return jsonify({"success": True, "lesson": updated_lesson})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating lesson: {str(e)}")
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@bp.route("/lessons/<lesson_id>", methods=["DELETE"])
|
||||||
|
@admin_required
|
||||||
|
def delete_lesson(lesson_id):
|
||||||
|
"""Delete a lesson"""
|
||||||
|
try:
|
||||||
|
print(f"Deleting lesson: {lesson_id}")
|
||||||
|
|
||||||
|
result = db.lessons.delete_one({"_id": ObjectId(lesson_id)})
|
||||||
|
|
||||||
|
if result.deleted_count == 0:
|
||||||
|
return jsonify({"error": "Lesson not found"}), 404
|
||||||
|
|
||||||
|
return jsonify({"success": True, "message": "Lesson deleted successfully"})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error deleting lesson: {str(e)}")
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
# ✅ LEGACY: For backward compatibility with old course structure
|
||||||
@bp.route("/courses/<course_id>/lessons", methods=["POST"])
|
@bp.route("/courses/<course_id>/lessons", methods=["POST"])
|
||||||
@admin_required
|
@admin_required
|
||||||
def add_lesson(course_id):
|
def add_lesson_legacy(course_id):
|
||||||
"""Add lesson to course"""
|
"""Add lesson to course (legacy endpoint)"""
|
||||||
try:
|
try:
|
||||||
data = request.json
|
data = request.json
|
||||||
|
|
||||||
@@ -382,6 +661,7 @@ def get_admin_stats():
|
|||||||
try:
|
try:
|
||||||
total_courses = db.courses.count_documents({})
|
total_courses = db.courses.count_documents({})
|
||||||
total_lessons = db.lessons.count_documents({})
|
total_lessons = db.lessons.count_documents({})
|
||||||
|
total_modules = db.modules.count_documents({})
|
||||||
|
|
||||||
# Course statistics by subject
|
# Course statistics by subject
|
||||||
pipeline = [
|
pipeline = [
|
||||||
@@ -398,6 +678,7 @@ def get_admin_stats():
|
|||||||
stats = {
|
stats = {
|
||||||
"total_courses": total_courses,
|
"total_courses": total_courses,
|
||||||
"total_lessons": total_lessons,
|
"total_lessons": total_lessons,
|
||||||
|
"total_modules": total_modules,
|
||||||
"subjects": subjects,
|
"subjects": subjects,
|
||||||
"difficulties": difficulties,
|
"difficulties": difficulties,
|
||||||
"last_updated": datetime.now().isoformat()
|
"last_updated": datetime.now().isoformat()
|
||||||
@@ -421,6 +702,16 @@ def admin_health():
|
|||||||
"POST /api/admin/courses",
|
"POST /api/admin/courses",
|
||||||
"PUT /api/admin/courses/<id>",
|
"PUT /api/admin/courses/<id>",
|
||||||
"DELETE /api/admin/courses/<id>",
|
"DELETE /api/admin/courses/<id>",
|
||||||
|
"GET /api/admin/courses/<course_id>/modules",
|
||||||
|
"POST /api/admin/courses/<course_id>/modules",
|
||||||
|
"GET /api/admin/modules/<module_id>",
|
||||||
|
"PUT /api/admin/modules/<module_id>",
|
||||||
|
"DELETE /api/admin/modules/<module_id>",
|
||||||
|
"GET /api/admin/modules/<module_id>/lessons",
|
||||||
|
"POST /api/admin/modules/<module_id>/lessons",
|
||||||
|
"GET /api/admin/lessons/<lesson_id>",
|
||||||
|
"PUT /api/admin/lessons/<lesson_id>",
|
||||||
|
"DELETE /api/admin/lessons/<lesson_id>",
|
||||||
"POST /api/admin/initialize",
|
"POST /api/admin/initialize",
|
||||||
"GET /api/admin/test",
|
"GET /api/admin/test",
|
||||||
"GET /api/admin/stats"
|
"GET /api/admin/stats"
|
||||||
|
|||||||
+621
-62
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { Plus, Edit, Trash2, Eye, RefreshCw } from 'lucide-react'
|
import { Plus, Edit, Trash2, Eye, RefreshCw, BookOpen, List, X } from 'lucide-react'
|
||||||
|
|
||||||
interface Course {
|
interface Course {
|
||||||
id: string
|
id: string
|
||||||
@@ -16,6 +16,25 @@ interface Course {
|
|||||||
created_at: string
|
created_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Module {
|
||||||
|
id: string
|
||||||
|
course_id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
order: number
|
||||||
|
created_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Lesson {
|
||||||
|
id: string
|
||||||
|
module_id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
video_url: string
|
||||||
|
order: number
|
||||||
|
created_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function AdminDashboard() {
|
export default function AdminDashboard() {
|
||||||
const [isClient, setIsClient] = useState(false)
|
const [isClient, setIsClient] = useState(false)
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||||
@@ -24,6 +43,16 @@ export default function AdminDashboard() {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [showAddForm, setShowAddForm] = useState(false)
|
const [showAddForm, setShowAddForm] = useState(false)
|
||||||
const [editingCourse, setEditingCourse] = useState<Course | null>(null)
|
const [editingCourse, setEditingCourse] = useState<Course | null>(null)
|
||||||
|
|
||||||
|
// Module/Lesson Management State
|
||||||
|
const [showModulesModal, setShowModulesModal] = useState(false)
|
||||||
|
const [selectedCourse, setSelectedCourse] = useState<Course | null>(null)
|
||||||
|
const [modules, setModules] = useState<Module[]>([])
|
||||||
|
const [lessons, setLessons] = useState<Lesson[]>([])
|
||||||
|
const [modulesLoading, setModulesLoading] = useState(false)
|
||||||
|
const [lessonsLoading, setLessonsLoading] = useState(false)
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||||
|
|
||||||
const [stats, setStats] = useState({
|
const [stats, setStats] = useState({
|
||||||
total_courses: 0,
|
total_courses: 0,
|
||||||
total_lessons: 0,
|
total_lessons: 0,
|
||||||
@@ -32,54 +61,43 @@ export default function AdminDashboard() {
|
|||||||
})
|
})
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// Enhanced authentication with API verification to prevent redirect loops
|
// Authentication logic
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsClient(true)
|
setIsClient(true)
|
||||||
|
|
||||||
const checkAuth = async () => {
|
const checkAuth = async () => {
|
||||||
try {
|
try {
|
||||||
// Add delay to prevent race conditions
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500))
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
const token = localStorage.getItem('admin_token')
|
const token = localStorage.getItem('admin_token')
|
||||||
console.log('Dashboard - checking token:', token)
|
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.log('Dashboard - no token found')
|
|
||||||
router.push('/admin/login')
|
router.push('/admin/login')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token === 'admin-secret-key') {
|
if (token === 'admin-secret-key') {
|
||||||
console.log('Dashboard - token format valid, verifying with API...')
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://127.0.0.1:5000/api/admin/courses', {
|
const response = await fetch('http://127.0.0.1:5000/api/admin/courses', {
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
console.log('✅ Dashboard - API confirms token valid')
|
|
||||||
setIsAuthenticated(true)
|
setIsAuthenticated(true)
|
||||||
fetchData()
|
fetchData()
|
||||||
} else {
|
} else {
|
||||||
console.log('❌ Dashboard - API rejects token')
|
|
||||||
localStorage.removeItem('admin_token')
|
localStorage.removeItem('admin_token')
|
||||||
router.push('/admin/login')
|
router.push('/admin/login')
|
||||||
}
|
}
|
||||||
} catch (apiError) {
|
} catch (apiError) {
|
||||||
console.error('Dashboard - API check failed:', apiError)
|
|
||||||
// Don't redirect on API error, might be temporary network issue
|
|
||||||
setIsAuthenticated(true)
|
setIsAuthenticated(true)
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('Dashboard - invalid token format')
|
|
||||||
localStorage.removeItem('admin_token')
|
localStorage.removeItem('admin_token')
|
||||||
router.push('/admin/login')
|
router.push('/admin/login')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Dashboard - auth check error:', error)
|
|
||||||
router.push('/admin/login')
|
router.push('/admin/login')
|
||||||
} finally {
|
} finally {
|
||||||
setAuthChecked(true)
|
setAuthChecked(true)
|
||||||
@@ -95,8 +113,6 @@ export default function AdminDashboard() {
|
|||||||
|
|
||||||
const fetchCourses = async () => {
|
const fetchCourses = async () => {
|
||||||
try {
|
try {
|
||||||
console.log('Fetching courses...')
|
|
||||||
|
|
||||||
const response = await fetch('http://127.0.0.1:5000/api/admin/courses', {
|
const response = await fetch('http://127.0.0.1:5000/api/admin/courses', {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': 'Bearer admin-secret-key',
|
'Authorization': 'Bearer admin-secret-key',
|
||||||
@@ -104,24 +120,18 @@ export default function AdminDashboard() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('Response status:', response.status)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`)
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
console.log('Received data:', data)
|
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
setCourses(data)
|
setCourses(data)
|
||||||
console.log('✅ Courses set successfully:', data.length, 'courses')
|
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ API returned non-array data:', data)
|
|
||||||
setCourses([])
|
setCourses([])
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error fetching courses:', error)
|
|
||||||
setCourses([])
|
setCourses([])
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -133,13 +143,112 @@ export default function AdminDashboard() {
|
|||||||
const response = await fetch('http://127.0.0.1:5000/api/admin/dashboard', {
|
const response = await fetch('http://127.0.0.1:5000/api/admin/dashboard', {
|
||||||
headers: { 'Authorization': 'Bearer admin-secret-key' }
|
headers: { 'Authorization': 'Bearer admin-secret-key' }
|
||||||
})
|
})
|
||||||
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
setStats(data)
|
setStats(data)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching stats:', error)
|
// Silent fail for stats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ FIXED: Module fetching - the key fix here
|
||||||
|
const fetchModules = async (courseId: string) => {
|
||||||
|
setModulesLoading(true)
|
||||||
|
setErrorMessage(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🔍 Fetching modules for course:', courseId) // Debug log
|
||||||
|
|
||||||
|
const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}/modules`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer admin-secret-key',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('🔍 Modules response status:', response.status) // Debug log
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
console.log('🔍 Modules response data:', data) // Debug log
|
||||||
|
|
||||||
|
// ✅ FIXED: Proper handling of modules response
|
||||||
|
let modulesList: Module[] = []
|
||||||
|
|
||||||
|
if (data.modules && Array.isArray(data.modules)) {
|
||||||
|
modulesList = data.modules
|
||||||
|
} else if (Array.isArray(data)) {
|
||||||
|
modulesList = data
|
||||||
|
} else if (data.success && data.data && Array.isArray(data.data)) {
|
||||||
|
modulesList = data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔍 Setting modules:', modulesList) // Debug log
|
||||||
|
setModules(modulesList)
|
||||||
|
} else {
|
||||||
|
console.error('❌ Failed to fetch modules:', response.status)
|
||||||
|
setModules([])
|
||||||
|
setErrorMessage(`Failed to load modules: ${response.status}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Network error fetching modules:', error)
|
||||||
|
setModules([])
|
||||||
|
setErrorMessage('Network error loading modules')
|
||||||
|
} finally {
|
||||||
|
setModulesLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ FIXED: Lesson fetching - the key fix here
|
||||||
|
const fetchLessons = async (moduleId: string) => {
|
||||||
|
setLessonsLoading(true)
|
||||||
|
setErrorMessage(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🔍 Fetching lessons for module:', moduleId) // Debug log
|
||||||
|
|
||||||
|
const response = await fetch(`http://127.0.0.1:5000/api/admin/modules/${moduleId}/lessons`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer admin-secret-key',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('🔍 Lessons response status:', response.status) // Debug log
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
console.log('🔍 Lessons response data:', data) // Debug log
|
||||||
|
|
||||||
|
// ✅ FIXED: Proper handling of lessons response
|
||||||
|
let lessonsList: Lesson[] = []
|
||||||
|
|
||||||
|
if (data.lessons && Array.isArray(data.lessons)) {
|
||||||
|
lessonsList = data.lessons
|
||||||
|
} else if (Array.isArray(data)) {
|
||||||
|
lessonsList = data
|
||||||
|
} else if (data.success && data.data && Array.isArray(data.data)) {
|
||||||
|
lessonsList = data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔍 Setting lessons:', lessonsList) // Debug log
|
||||||
|
setLessons(lessonsList)
|
||||||
|
} else {
|
||||||
|
console.error('❌ Failed to fetch lessons:', response.status)
|
||||||
|
setLessons([])
|
||||||
|
setErrorMessage(`Failed to load lessons: ${response.status}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Network error fetching lessons:', error)
|
||||||
|
setLessons([])
|
||||||
|
setErrorMessage('Network error loading lessons')
|
||||||
|
} finally {
|
||||||
|
setLessonsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Course CRUD operations
|
||||||
const handleCreateCourse = async (formData: any) => {
|
const handleCreateCourse = async (formData: any) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://127.0.0.1:5000/api/admin/courses', {
|
const response = await fetch('http://127.0.0.1:5000/api/admin/courses', {
|
||||||
@@ -157,10 +266,9 @@ export default function AdminDashboard() {
|
|||||||
alert('Course created successfully!')
|
alert('Course created successfully!')
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json()
|
const error = await response.json()
|
||||||
alert(`Error: ${error.error}`)
|
alert(`Error: ${error.error || 'Failed to create course'}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating course:', error)
|
|
||||||
alert('Failed to create course')
|
alert('Failed to create course')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,17 +289,15 @@ export default function AdminDashboard() {
|
|||||||
setEditingCourse(null)
|
setEditingCourse(null)
|
||||||
alert('Course updated successfully!')
|
alert('Course updated successfully!')
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json()
|
alert('Failed to update course')
|
||||||
alert(`Error: ${error.error}`)
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating course:', error)
|
|
||||||
alert('Failed to update course')
|
alert('Failed to update course')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteCourse = async (courseId: string) => {
|
const handleDeleteCourse = async (courseId: string) => {
|
||||||
if (confirm('Are you sure you want to delete this course? This action cannot be undone.')) {
|
if (confirm('Are you sure you want to delete this course?')) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}`, {
|
const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -202,29 +308,72 @@ export default function AdminDashboard() {
|
|||||||
await fetchData()
|
await fetchData()
|
||||||
alert('Course deleted successfully!')
|
alert('Course deleted successfully!')
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json()
|
alert('Failed to delete course')
|
||||||
alert(`Error: ${error.error}`)
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting course:', error)
|
|
||||||
alert('Failed to delete course')
|
alert('Failed to delete course')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initializeDefaultCourses = async () => {
|
// Module/Lesson Management
|
||||||
|
const openModulesManager = async (course: Course) => {
|
||||||
|
console.log('🔍 Opening modules manager for course:', course.id)
|
||||||
|
setSelectedCourse(course)
|
||||||
|
setShowModulesModal(true)
|
||||||
|
setModules([])
|
||||||
|
setLessons([])
|
||||||
|
setErrorMessage(null)
|
||||||
|
await fetchModules(course.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreateModule = async (formData: any) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://127.0.0.1:5000/api/admin/initialize', {
|
console.log('🔍 Creating module with data:', formData)
|
||||||
|
|
||||||
|
const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${selectedCourse?.id}/modules`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Authorization': 'Bearer admin-secret-key' }
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer admin-secret-key'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
await fetchData()
|
await fetchModules(selectedCourse!.id)
|
||||||
alert('Default courses initialized!')
|
alert('Module created successfully!')
|
||||||
|
} else {
|
||||||
|
const error = await response.json()
|
||||||
|
alert(`Error: ${error.error || 'Failed to create module'}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing courses:', error)
|
alert('Failed to create module')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreateLesson = async (moduleId: string, formData: any) => {
|
||||||
|
try {
|
||||||
|
console.log('🔍 Creating lesson for module:', moduleId, 'with data:', formData)
|
||||||
|
|
||||||
|
const response = await fetch(`http://127.0.0.1:5000/api/admin/modules/${moduleId}/lessons`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer admin-secret-key'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
await fetchLessons(moduleId)
|
||||||
|
alert('Lesson created successfully!')
|
||||||
|
} else {
|
||||||
|
const error = await response.json()
|
||||||
|
alert(`Error: ${error.error || 'Failed to create lesson'}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Failed to create lesson')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +382,6 @@ export default function AdminDashboard() {
|
|||||||
router.push('/')
|
router.push('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show loading until auth is checked
|
|
||||||
if (!isClient || !authChecked) {
|
if (!isClient || !authChecked) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
@@ -245,7 +393,6 @@ export default function AdminDashboard() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show redirect message if not authenticated
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
@@ -280,7 +427,7 @@ export default function AdminDashboard() {
|
|||||||
>
|
>
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<span className="text-gray-600">Welcome, 5t4l1n! 👋</span>
|
<span className="text-gray-600">Welcome, Admin! 👋</span>
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded text-sm"
|
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded text-sm"
|
||||||
@@ -294,19 +441,26 @@ export default function AdminDashboard() {
|
|||||||
|
|
||||||
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||||
<div className="px-4 py-6 sm:px-0">
|
<div className="px-4 py-6 sm:px-0">
|
||||||
|
{/* Error Display */}
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">
|
||||||
|
<span className="block sm:inline">{errorMessage}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setErrorMessage(null)}
|
||||||
|
className="absolute top-0 bottom-0 right-0 px-4 py-3"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-gray-900">Course Management</h2>
|
<h2 className="text-2xl font-bold text-gray-900">Course Management</h2>
|
||||||
<p className="text-gray-600">Add, edit, or remove courses dynamically</p>
|
<p className="text-gray-600">Manage courses, modules, and lessons</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
<button
|
|
||||||
onClick={initializeDefaultCourses}
|
|
||||||
className="bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded-lg flex items-center space-x-2"
|
|
||||||
>
|
|
||||||
<span>Initialize Default Courses</span>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowAddForm(true)}
|
onClick={() => setShowAddForm(true)}
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center space-x-2"
|
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center space-x-2"
|
||||||
@@ -348,15 +502,9 @@ export default function AdminDashboard() {
|
|||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||||
<p className="mt-2 text-gray-600">Loading courses...</p>
|
<p className="mt-2 text-gray-600">Loading courses...</p>
|
||||||
</div>
|
</div>
|
||||||
) : !Array.isArray(courses) || courses.length === 0 ? (
|
) : courses.length === 0 ? (
|
||||||
<div className="p-8 text-center">
|
<div className="p-8 text-center">
|
||||||
<p className="text-gray-500 mb-4">No courses found. Initialize default courses or add a new one.</p>
|
<p className="text-gray-500 mb-4">No courses found.</p>
|
||||||
<button
|
|
||||||
onClick={initializeDefaultCourses}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded"
|
|
||||||
>
|
|
||||||
Initialize Default Courses
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
@@ -419,6 +567,13 @@ export default function AdminDashboard() {
|
|||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => openModulesManager(course)}
|
||||||
|
className="text-purple-600 hover:text-purple-900"
|
||||||
|
title="Manage Modules & Lessons"
|
||||||
|
>
|
||||||
|
<BookOpen className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -431,7 +586,7 @@ export default function AdminDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Add Course Modal */}
|
{/* Course Form Modal */}
|
||||||
{showAddForm && (
|
{showAddForm && (
|
||||||
<CourseFormModal
|
<CourseFormModal
|
||||||
title="Add New Course"
|
title="Add New Course"
|
||||||
@@ -440,7 +595,6 @@ export default function AdminDashboard() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Edit Course Modal */}
|
|
||||||
{editingCourse && (
|
{editingCourse && (
|
||||||
<CourseFormModal
|
<CourseFormModal
|
||||||
title="Edit Course"
|
title="Edit Course"
|
||||||
@@ -449,11 +603,419 @@ export default function AdminDashboard() {
|
|||||||
onSubmit={(data) => handleUpdateCourse(editingCourse.id, data)}
|
onSubmit={(data) => handleUpdateCourse(editingCourse.id, data)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ✅ FIXED: Modules & Lessons Modal */}
|
||||||
|
{showModulesModal && selectedCourse && (
|
||||||
|
<ModulesLessonsModal
|
||||||
|
course={selectedCourse}
|
||||||
|
modules={modules}
|
||||||
|
lessons={lessons}
|
||||||
|
modulesLoading={modulesLoading}
|
||||||
|
lessonsLoading={lessonsLoading}
|
||||||
|
onClose={() => {
|
||||||
|
setShowModulesModal(false)
|
||||||
|
setSelectedCourse(null)
|
||||||
|
setModules([])
|
||||||
|
setLessons([])
|
||||||
|
setErrorMessage(null)
|
||||||
|
}}
|
||||||
|
onCreateModule={handleCreateModule}
|
||||||
|
onCreateLesson={handleCreateLesson}
|
||||||
|
onFetchLessons={fetchLessons}
|
||||||
|
onRefreshModules={() => fetchModules(selectedCourse.id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Course Form Modal Component
|
// ✅ FIXED: Enhanced Modules & Lessons Modal with better debugging
|
||||||
|
function ModulesLessonsModal({
|
||||||
|
course,
|
||||||
|
modules,
|
||||||
|
lessons,
|
||||||
|
modulesLoading,
|
||||||
|
lessonsLoading,
|
||||||
|
onClose,
|
||||||
|
onCreateModule,
|
||||||
|
onCreateLesson,
|
||||||
|
onFetchLessons,
|
||||||
|
onRefreshModules
|
||||||
|
}: {
|
||||||
|
course: Course
|
||||||
|
modules: Module[]
|
||||||
|
lessons: Lesson[]
|
||||||
|
modulesLoading: boolean
|
||||||
|
lessonsLoading: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onCreateModule: (data: any) => void
|
||||||
|
onCreateLesson: (moduleId: string, data: any) => void
|
||||||
|
onFetchLessons: (moduleId: string) => void
|
||||||
|
onRefreshModules: () => void
|
||||||
|
}) {
|
||||||
|
const [activeTab, setActiveTab] = useState<'modules' | 'lessons'>('modules')
|
||||||
|
const [showModuleForm, setShowModuleForm] = useState(false)
|
||||||
|
const [showLessonForm, setShowLessonForm] = useState(false)
|
||||||
|
const [selectedModuleId, setSelectedModuleId] = useState<string | null>(null)
|
||||||
|
const [moduleFormData, setModuleFormData] = useState({ title: '', description: '', order: 1 })
|
||||||
|
const [lessonFormData, setLessonFormData] = useState({ title: '', description: '', video_url: '', order: 1 })
|
||||||
|
|
||||||
|
const handleModuleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onCreateModule(moduleFormData)
|
||||||
|
setModuleFormData({ title: '', description: '', order: 1 })
|
||||||
|
setShowModuleForm(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLessonSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
console.log('🔍 Creating lesson for module ID:', selectedModuleId) // Debug log
|
||||||
|
console.log('🔍 Lesson form data:', lessonFormData) // Debug log
|
||||||
|
if (selectedModuleId) {
|
||||||
|
onCreateLesson(selectedModuleId, lessonFormData)
|
||||||
|
setLessonFormData({ title: '', description: '', video_url: '', order: 1 })
|
||||||
|
setShowLessonForm(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ FIXED: Enhanced module selection with better debugging
|
||||||
|
const handleSelectModule = (moduleId: string) => {
|
||||||
|
console.log('🔍 Selecting module with ID:', moduleId) // Debug log
|
||||||
|
console.log('🔍 Available modules:', modules) // Debug log
|
||||||
|
setSelectedModuleId(moduleId)
|
||||||
|
onFetchLessons(moduleId)
|
||||||
|
setActiveTab('lessons')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-5xl m-4 max-h-[90vh] overflow-y-auto">
|
||||||
|
<div className="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">
|
||||||
|
Manage: {course.title}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={onRefreshModules}
|
||||||
|
className="p-1 text-gray-500 hover:text-gray-700 rounded"
|
||||||
|
title="Refresh Modules"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6">
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className="flex border-b border-gray-200 mb-6">
|
||||||
|
<button
|
||||||
|
className={`py-2 px-4 ${activeTab === 'modules' ? 'border-b-2 border-blue-500 text-blue-600' : 'text-gray-500'}`}
|
||||||
|
onClick={() => setActiveTab('modules')}
|
||||||
|
>
|
||||||
|
Modules ({modules.length})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`py-2 px-4 ml-4 ${activeTab === 'lessons' ? 'border-b-2 border-blue-500 text-blue-600' : 'text-gray-500'}`}
|
||||||
|
onClick={() => setActiveTab('lessons')}
|
||||||
|
>
|
||||||
|
Lessons ({lessons.length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ✅ DEBUG INFO - Remove after fixing */}
|
||||||
|
<div className="mb-4 p-3 bg-gray-100 rounded text-xs">
|
||||||
|
<p><strong>Debug Info:</strong></p>
|
||||||
|
<p>Selected Module ID: {selectedModuleId || 'None'}</p>
|
||||||
|
<p>Modules Count: {modules.length}</p>
|
||||||
|
<p>Lessons Count: {lessons.length}</p>
|
||||||
|
<p>Active Tab: {activeTab}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modules Tab */}
|
||||||
|
{activeTab === 'modules' && (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h4 className="text-lg font-semibold">Course Modules</h4>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowModuleForm(true)}
|
||||||
|
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span>Add Module</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{modulesLoading ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
|
||||||
|
<p className="text-gray-600">Loading modules...</p>
|
||||||
|
</div>
|
||||||
|
) : modules.length === 0 ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<p className="text-gray-500 mb-4">No modules found for this course.</p>
|
||||||
|
<p className="text-sm text-gray-400">Click "Add Module" to create your first module.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{modules.map((module, index) => (
|
||||||
|
<div key={module.id || index} className="border rounded-lg p-4 hover:bg-gray-50">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h5 className="font-semibold text-gray-900">{module.title}</h5>
|
||||||
|
<p className="text-gray-600 text-sm mt-1">{module.description}</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">Order: {module.order} | ID: {module.id}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleSelectModule(module.id)}
|
||||||
|
className="text-blue-600 hover:text-blue-800 text-sm p-1"
|
||||||
|
title="View Lessons"
|
||||||
|
>
|
||||||
|
<List className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="text-green-600 hover:text-green-800 text-sm p-1"
|
||||||
|
title="Edit Module"
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="text-red-600 hover:text-red-800 text-sm p-1"
|
||||||
|
title="Delete Module"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Module Form */}
|
||||||
|
{showModuleForm && (
|
||||||
|
<div className="mt-6 border-t pt-6">
|
||||||
|
<h5 className="font-semibold mb-4">Add New Module</h5>
|
||||||
|
<form onSubmit={handleModuleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Module Title *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={moduleFormData.title}
|
||||||
|
onChange={(e) => setModuleFormData({...moduleFormData, title: e.target.value})}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="e.g., Introduction to Python"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={moduleFormData.description}
|
||||||
|
onChange={(e) => setModuleFormData({...moduleFormData, description: e.target.value})}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
rows={3}
|
||||||
|
placeholder="Module description..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Order
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={moduleFormData.order}
|
||||||
|
onChange={(e) => setModuleFormData({...moduleFormData, order: parseInt(e.target.value) || 1})}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-3">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Create Module
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowModuleForm(false)}
|
||||||
|
className="bg-gray-300 text-gray-700 px-4 py-2 rounded hover:bg-gray-400"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Lessons Tab */}
|
||||||
|
{activeTab === 'lessons' && (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h4 className="text-lg font-semibold">
|
||||||
|
Lessons {selectedModuleId ? `(${modules.find(m => m.id === selectedModuleId)?.title})` : ''}
|
||||||
|
</h4>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowLessonForm(true)}
|
||||||
|
disabled={!selectedModuleId}
|
||||||
|
className="bg-purple-600 text-white px-4 py-2 rounded hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span>Add Lesson</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!selectedModuleId ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<p className="text-gray-500">Select a module from the Modules tab to view/add lessons</p>
|
||||||
|
</div>
|
||||||
|
) : lessonsLoading ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600 mx-auto mb-2"></div>
|
||||||
|
<p className="text-gray-600">Loading lessons...</p>
|
||||||
|
</div>
|
||||||
|
) : lessons.length === 0 ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<p className="text-gray-500 mb-4">No lessons found for this module.</p>
|
||||||
|
<p className="text-sm text-gray-400">Click "Add Lesson" to create your first lesson.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{lessons.map((lesson, index) => (
|
||||||
|
<div key={lesson.id || index} className="border rounded-lg p-4 hover:bg-gray-50">
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h5 className="font-semibold text-gray-900">{lesson.title}</h5>
|
||||||
|
<p className="text-gray-600 text-sm mt-1">{lesson.description}</p>
|
||||||
|
<div className="flex items-center space-x-4 mt-2">
|
||||||
|
<p className="text-xs text-gray-400">Order: {lesson.order}</p>
|
||||||
|
<p className="text-xs text-gray-400">ID: {lesson.id}</p>
|
||||||
|
{lesson.video_url && (
|
||||||
|
<a
|
||||||
|
href={lesson.video_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 text-xs hover:underline"
|
||||||
|
>
|
||||||
|
📹 Video Link
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
className="text-green-600 hover:text-green-800 text-sm p-1"
|
||||||
|
title="Edit Lesson"
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="text-red-600 hover:text-red-800 text-sm p-1"
|
||||||
|
title="Delete Lesson"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Lesson Form */}
|
||||||
|
{showLessonForm && selectedModuleId && (
|
||||||
|
<div className="mt-6 border-t pt-6">
|
||||||
|
<h5 className="font-semibold mb-4">Add New Lesson</h5>
|
||||||
|
<form onSubmit={handleLessonSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Lesson Title *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={lessonFormData.title}
|
||||||
|
onChange={(e) => setLessonFormData({...lessonFormData, title: e.target.value})}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
placeholder="e.g., Variables and Data Types"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={lessonFormData.description}
|
||||||
|
onChange={(e) => setLessonFormData({...lessonFormData, description: e.target.value})}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
rows={3}
|
||||||
|
placeholder="Lesson description..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Video URL
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={lessonFormData.video_url}
|
||||||
|
onChange={(e) => setLessonFormData({...lessonFormData, video_url: e.target.value})}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
placeholder="https://youtu.be/..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Order
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={lessonFormData.order}
|
||||||
|
onChange={(e) => setLessonFormData({...lessonFormData, order: parseInt(e.target.value) || 1})}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-3">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-purple-600 text-white px-4 py-2 rounded hover:bg-purple-700"
|
||||||
|
>
|
||||||
|
Create Lesson
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowLessonForm(false)}
|
||||||
|
className="bg-gray-300 text-gray-700 px-4 py-2 rounded hover:bg-gray-400"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Course Form Modal (unchanged)
|
||||||
function CourseFormModal({
|
function CourseFormModal({
|
||||||
title,
|
title,
|
||||||
course,
|
course,
|
||||||
@@ -470,7 +1032,7 @@ function CourseFormModal({
|
|||||||
subject: course?.subject || 'Programming',
|
subject: course?.subject || 'Programming',
|
||||||
description: course?.description || '',
|
description: course?.description || '',
|
||||||
difficulty: course?.difficulty || 'Beginner',
|
difficulty: course?.difficulty || 'Beginner',
|
||||||
mentor: course?.mentor || '5t4l1n',
|
mentor: course?.mentor || 'Admin',
|
||||||
video_url: course?.video_url || ''
|
video_url: course?.video_url || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -495,7 +1057,6 @@ function CourseFormModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
|
||||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50">
|
||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl m-4 max-h-screen overflow-y-auto">
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl m-4 max-h-screen overflow-y-auto">
|
||||||
<div className="px-6 py-4 border-b border-gray-200">
|
<div className="px-6 py-4 border-b border-gray-200">
|
||||||
@@ -607,7 +1168,6 @@ function CourseFormModal({
|
|||||||
title="Video Preview"
|
title="Video Preview"
|
||||||
className="w-full h-full"
|
className="w-full h-full"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -631,6 +1191,5 @@ function CourseFormModal({
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,43 @@ import api from "@/lib/api"
|
|||||||
import { CourseSidebar } from "@/components/course-sidebar"
|
import { CourseSidebar } from "@/components/course-sidebar"
|
||||||
import { LessonViewer } from "@/components/lesson-viewer"
|
import { LessonViewer } from "@/components/lesson-viewer"
|
||||||
import { Loader2 } from "lucide-react"
|
import { Loader2 } from "lucide-react"
|
||||||
import type { Course } from "@/lib/types"
|
|
||||||
|
interface Course {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
subject: string
|
||||||
|
description: string
|
||||||
|
difficulty: string
|
||||||
|
mentor: string
|
||||||
|
video_url: string
|
||||||
|
embed_url: string
|
||||||
|
students: number
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Module {
|
||||||
|
id: string
|
||||||
|
course_id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
order: number
|
||||||
|
created_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Lesson {
|
||||||
|
id: string
|
||||||
|
module_id: string
|
||||||
|
course_id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
video_url: string
|
||||||
|
embed_url: string
|
||||||
|
order: number
|
||||||
|
duration?: string
|
||||||
|
type: string
|
||||||
|
content?: string
|
||||||
|
created_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function LessonDetailPage() {
|
export default function LessonDetailPage() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
@@ -18,6 +54,9 @@ export default function LessonDetailPage() {
|
|||||||
const { user, firebaseUser, isLoading: isAuthLoading } = useAuth()
|
const { user, firebaseUser, isLoading: isAuthLoading } = useAuth()
|
||||||
|
|
||||||
const [course, setCourse] = useState<Course | null>(null)
|
const [course, setCourse] = useState<Course | null>(null)
|
||||||
|
const [modules, setModules] = useState<Module[]>([])
|
||||||
|
const [lessons, setLessons] = useState<{ [moduleId: string]: Lesson[] }>({})
|
||||||
|
const [currentLesson, setCurrentLesson] = useState<Lesson | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
@@ -29,22 +68,96 @@ export default function LessonDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ((user || firebaseUser) && courseId) {
|
if ((user || firebaseUser) && courseId) {
|
||||||
const fetchCourse = async () => {
|
fetchCourseData()
|
||||||
|
}
|
||||||
|
}, [user, firebaseUser, isAuthLoading, router, courseId])
|
||||||
|
|
||||||
|
const fetchCourseData = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.get<Course>(`/api/courses/${courseId}?t=${Date.now()}`)
|
console.log('🔍 Fetching course data for:', courseId)
|
||||||
setCourse(response.data)
|
|
||||||
|
// Fetch course details
|
||||||
|
const courseResponse = await api.get<Course>(`/api/courses/${courseId}?t=${Date.now()}`)
|
||||||
|
const courseData = courseResponse.data
|
||||||
|
console.log('✅ Course data loaded:', courseData)
|
||||||
|
setCourse(courseData)
|
||||||
|
|
||||||
|
// Fetch modules for the course
|
||||||
|
const modulesResponse = await fetch(`http://127.0.0.1:5000/api/courses/${courseId}/modules`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (modulesResponse.ok) {
|
||||||
|
const modulesData = await modulesResponse.json()
|
||||||
|
console.log('✅ Modules data loaded:', modulesData)
|
||||||
|
|
||||||
|
let modulesList: Module[] = []
|
||||||
|
if (modulesData.modules && Array.isArray(modulesData.modules)) {
|
||||||
|
modulesList = modulesData.modules
|
||||||
|
} else if (Array.isArray(modulesData)) {
|
||||||
|
modulesList = modulesData
|
||||||
|
}
|
||||||
|
|
||||||
|
setModules(modulesList)
|
||||||
|
|
||||||
|
// Fetch lessons for all modules
|
||||||
|
const lessonsData: { [moduleId: string]: Lesson[] } = {}
|
||||||
|
let foundCurrentLesson: Lesson | null = null
|
||||||
|
|
||||||
|
for (const module of modulesList) {
|
||||||
|
const lessonsResponse = await fetch(`http://127.0.0.1:5000/api/modules/${module.id}/lessons`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (lessonsResponse.ok) {
|
||||||
|
const lessonData = await lessonsResponse.json()
|
||||||
|
console.log(`✅ Lessons loaded for module ${module.id}:`, lessonData)
|
||||||
|
|
||||||
|
let lessonsList: Lesson[] = []
|
||||||
|
if (lessonData.lessons && Array.isArray(lessonData.lessons)) {
|
||||||
|
lessonsList = lessonData.lessons
|
||||||
|
} else if (Array.isArray(lessonData)) {
|
||||||
|
lessonsList = lessonData
|
||||||
|
}
|
||||||
|
|
||||||
|
lessonsData[module.id] = lessonsList
|
||||||
|
|
||||||
|
// Find the current lesson
|
||||||
|
if (lessonId && !foundCurrentLesson) {
|
||||||
|
foundCurrentLesson = lessonsList.find(lesson => lesson.id === lessonId) || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLessons(lessonsData)
|
||||||
|
|
||||||
|
if (foundCurrentLesson) {
|
||||||
|
setCurrentLesson(foundCurrentLesson)
|
||||||
|
console.log('✅ Current lesson found:', foundCurrentLesson)
|
||||||
|
} else if (lessonId) {
|
||||||
|
console.log('❌ Lesson not found:', lessonId)
|
||||||
|
toast.error("Lesson not found")
|
||||||
|
router.replace(`/courses/${courseId}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to fetch modules')
|
||||||
|
}
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
console.error('❌ Error fetching course data:', err)
|
||||||
setError(err.message || "Failed to load course.")
|
setError(err.message || "Failed to load course.")
|
||||||
toast.error(err.message || "Failed to load course.")
|
toast.error(err.message || "Failed to load course.")
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchCourse()
|
|
||||||
}
|
|
||||||
}, [user, firebaseUser, isAuthLoading, router, courseId])
|
|
||||||
|
|
||||||
if (isAuthLoading || loading) {
|
if (isAuthLoading || loading) {
|
||||||
return (
|
return (
|
||||||
@@ -57,25 +170,62 @@ export default function LessonDetailPage() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center min-h-screen bg-white text-red-600">
|
<div className="flex flex-col justify-center items-center min-h-screen bg-white text-red-600">
|
||||||
<p>{error}</p>
|
<p className="text-lg mb-4">{error}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => router.back()}
|
||||||
|
className="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
|
||||||
|
>
|
||||||
|
Go Back
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!course) {
|
if (!course) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center min-h-screen bg-white text-gray-700">
|
<div className="flex flex-col justify-center items-center min-h-screen bg-white text-gray-700">
|
||||||
<p>Course not found.</p>
|
<p className="text-lg mb-4">Course not found.</p>
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/courses')}
|
||||||
|
className="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
|
||||||
|
>
|
||||||
|
Browse Courses
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentLesson) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col justify-center items-center min-h-screen bg-white text-gray-700">
|
||||||
|
<p className="text-lg mb-4">Lesson not found.</p>
|
||||||
|
<button
|
||||||
|
onClick={() => router.push(`/courses/${courseId}`)}
|
||||||
|
className="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
|
||||||
|
>
|
||||||
|
Back to Course
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col md:flex-row min-h-screen bg-gray-50">
|
<div className="flex flex-col md:flex-row min-h-screen bg-gray-50">
|
||||||
<CourseSidebar courseId={course.id} modules={course.modules} activeLessonId={lessonId} />
|
<CourseSidebar
|
||||||
|
courseId={course.id}
|
||||||
|
modules={modules}
|
||||||
|
lessons={lessons}
|
||||||
|
activeLessonId={lessonId}
|
||||||
|
currentLesson={currentLesson}
|
||||||
|
/>
|
||||||
<main className="flex-grow p-8 max-w-4xl mx-auto w-full">
|
<main className="flex-grow p-8 max-w-4xl mx-auto w-full">
|
||||||
<LessonViewer courseId={course.id} lessonId={lessonId} />
|
<LessonViewer
|
||||||
|
courseId={course.id}
|
||||||
|
lessonId={lessonId}
|
||||||
|
lesson={currentLesson}
|
||||||
|
course={course}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,31 +2,46 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { useRouter, useParams } from "next/navigation"
|
import { useRouter, useParams } from "next/navigation"
|
||||||
import { Loader2 } from "lucide-react"
|
import { Loader2, Play, Clock, BookOpen, ChevronDown, ChevronRight, User, Users, Star } from "lucide-react"
|
||||||
import { toast } from "react-hot-toast"
|
import { toast } from "react-hot-toast"
|
||||||
import api from "@/lib/api"
|
import api from "@/lib/api"
|
||||||
import { useAuth } from "@/context/auth-context"
|
import { useAuth } from "@/context/auth-context"
|
||||||
|
|
||||||
type Lesson = {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
description?: string
|
|
||||||
video_url?: string
|
|
||||||
}
|
|
||||||
type Module = {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
lessons: Lesson[]
|
|
||||||
}
|
|
||||||
type Course = {
|
type Course = {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
modules: Module[]
|
subject: string
|
||||||
|
difficulty: string
|
||||||
|
mentor: string
|
||||||
|
students: number
|
||||||
embed_url?: string
|
embed_url?: string
|
||||||
video_url?: string
|
video_url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Module = {
|
||||||
|
id: string
|
||||||
|
course_id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
order: number
|
||||||
|
created_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Lesson = {
|
||||||
|
id: string
|
||||||
|
module_id: string
|
||||||
|
course_id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
video_url: string
|
||||||
|
embed_url: string
|
||||||
|
order: number
|
||||||
|
duration?: string
|
||||||
|
type: string
|
||||||
|
content?: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function CoursePage() {
|
export default function CoursePage() {
|
||||||
const { user, firebaseUser, isLoading: authLoading } = useAuth()
|
const { user, firebaseUser, isLoading: authLoading } = useAuth()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
@@ -34,12 +49,16 @@ export default function CoursePage() {
|
|||||||
const courseId = params?.courseId as string
|
const courseId = params?.courseId as string
|
||||||
|
|
||||||
const [course, setCourse] = useState<Course | null>(null)
|
const [course, setCourse] = useState<Course | null>(null)
|
||||||
|
const [modules, setModules] = useState<Module[]>([])
|
||||||
|
const [lessons, setLessons] = useState<{ [moduleId: string]: Lesson[] }>({})
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [modulesLoading, setModulesLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
// Sidebar state: current
|
// Navigation state
|
||||||
const [selectedModuleIdx, setSelectedModuleIdx] = useState(0)
|
const [selectedModuleId, setSelectedModuleId] = useState<string | null>(null)
|
||||||
const [selectedLessonIdx, setSelectedLessonIdx] = useState(0)
|
const [selectedLessonId, setSelectedLessonId] = useState<string | null>(null)
|
||||||
|
const [expandedModules, setExpandedModules] = useState<{ [moduleId: string]: boolean }>({})
|
||||||
const [completed, setCompleted] = useState(false)
|
const [completed, setCompleted] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -49,23 +68,191 @@ export default function CoursePage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ((user || firebaseUser) && courseId) {
|
if ((user || firebaseUser) && courseId) {
|
||||||
;(async () => {
|
fetchCourseData()
|
||||||
|
}
|
||||||
|
}, [authLoading, user, firebaseUser, courseId, router])
|
||||||
|
|
||||||
|
const fetchCourseData = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await api.get<Course>(`/api/courses/${courseId}?t=${Date.now()}`)
|
console.log('🔍 Starting to fetch course data for:', courseId)
|
||||||
setCourse(resp.data)
|
|
||||||
setSelectedModuleIdx(0)
|
// Fetch course details
|
||||||
setSelectedLessonIdx(0)
|
const courseResponse = await api.get<Course>(`/api/courses/${courseId}?t=${Date.now()}`)
|
||||||
setCompleted(false)
|
const courseData = courseResponse.data
|
||||||
} catch {
|
console.log('✅ Course data loaded:', courseData)
|
||||||
setError("Failed to load course data.")
|
setCourse(courseData)
|
||||||
|
|
||||||
|
// ✅ FIXED: Better module fetching with multiple endpoint attempts
|
||||||
|
await fetchModulesAndLessons(courseId)
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('❌ Error fetching course data:', err)
|
||||||
|
setError(err.message || "Failed to load course data.")
|
||||||
|
toast.error("Failed to load course data.")
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
})()
|
|
||||||
}
|
}
|
||||||
}, [authLoading, user, firebaseUser, courseId, router])
|
|
||||||
|
const fetchModulesAndLessons = async (courseId: string) => {
|
||||||
|
setModulesLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🔍 Fetching modules for course:', courseId)
|
||||||
|
|
||||||
|
// Try multiple endpoints for modules
|
||||||
|
let modulesData = null
|
||||||
|
let modulesResponse = null
|
||||||
|
|
||||||
|
// Try admin endpoint first (most likely to work based on previous conversation)
|
||||||
|
try {
|
||||||
|
modulesResponse = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}/modules`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer admin-secret-key',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (modulesResponse.ok) {
|
||||||
|
modulesData = await modulesResponse.json()
|
||||||
|
console.log('✅ Modules loaded from admin endpoint:', modulesData)
|
||||||
|
}
|
||||||
|
} catch (adminError) {
|
||||||
|
console.log('⚠️ Admin endpoint failed, trying public endpoint')
|
||||||
|
}
|
||||||
|
|
||||||
|
// If admin endpoint failed, try public endpoint
|
||||||
|
if (!modulesData || !modulesResponse?.ok) {
|
||||||
|
try {
|
||||||
|
modulesResponse = await fetch(`http://127.0.0.1:5000/api/courses/${courseId}/modules`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (modulesResponse.ok) {
|
||||||
|
modulesData = await modulesResponse.json()
|
||||||
|
console.log('✅ Modules loaded from public endpoint:', modulesData)
|
||||||
|
}
|
||||||
|
} catch (publicError) {
|
||||||
|
console.error('❌ Both module endpoints failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modulesData) {
|
||||||
|
let modulesList: Module[] = []
|
||||||
|
|
||||||
|
// Handle different response formats
|
||||||
|
if (modulesData.success && modulesData.modules && Array.isArray(modulesData.modules)) {
|
||||||
|
modulesList = modulesData.modules
|
||||||
|
} else if (modulesData.modules && Array.isArray(modulesData.modules)) {
|
||||||
|
modulesList = modulesData.modules
|
||||||
|
} else if (Array.isArray(modulesData)) {
|
||||||
|
modulesList = modulesData
|
||||||
|
} else if (modulesData.data && Array.isArray(modulesData.data)) {
|
||||||
|
modulesList = modulesData.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort modules by order
|
||||||
|
modulesList = modulesList.sort((a, b) => a.order - b.order)
|
||||||
|
|
||||||
|
console.log('🔍 Processed modules list:', modulesList)
|
||||||
|
setModules(modulesList)
|
||||||
|
|
||||||
|
// Fetch lessons for all modules
|
||||||
|
if (modulesList.length > 0) {
|
||||||
|
await fetchLessonsForAllModules(modulesList)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ No modules data received')
|
||||||
|
setModules([])
|
||||||
|
setLessons({})
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error in fetchModulesAndLessons:', error)
|
||||||
|
setModules([])
|
||||||
|
setLessons({})
|
||||||
|
} finally {
|
||||||
|
setModulesLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchLessonsForAllModules = async (modulesList: Module[]) => {
|
||||||
|
const lessonsData: { [moduleId: string]: Lesson[] } = {}
|
||||||
|
const expandedState: { [moduleId: string]: boolean } = {}
|
||||||
|
|
||||||
|
for (const module of modulesList) {
|
||||||
|
try {
|
||||||
|
console.log('🔍 Fetching lessons for module:', module.id)
|
||||||
|
|
||||||
|
// Try admin endpoint first
|
||||||
|
let lessonsResponse = await fetch(`http://127.0.0.1:5000/api/admin/modules/${module.id}/lessons`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer admin-secret-key',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// If admin fails, try public endpoint
|
||||||
|
if (!lessonsResponse.ok) {
|
||||||
|
lessonsResponse = await fetch(`http://127.0.0.1:5000/api/modules/${module.id}/lessons`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lessonsResponse.ok) {
|
||||||
|
const lessonData = await lessonsResponse.json()
|
||||||
|
console.log(`✅ Lessons loaded for module ${module.id}:`, lessonData)
|
||||||
|
|
||||||
|
let lessonsList: Lesson[] = []
|
||||||
|
if (lessonData.success && lessonData.lessons && Array.isArray(lessonData.lessons)) {
|
||||||
|
lessonsList = lessonData.lessons
|
||||||
|
} else if (lessonData.lessons && Array.isArray(lessonData.lessons)) {
|
||||||
|
lessonsList = lessonData.lessons
|
||||||
|
} else if (Array.isArray(lessonData)) {
|
||||||
|
lessonsList = lessonData
|
||||||
|
} else if (lessonData.data && Array.isArray(lessonData.data)) {
|
||||||
|
lessonsList = lessonData.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort lessons by order
|
||||||
|
lessonsList = lessonsList.sort((a, b) => a.order - b.order)
|
||||||
|
lessonsData[module.id] = lessonsList
|
||||||
|
|
||||||
|
// Auto-expand first module with lessons
|
||||||
|
if (!selectedModuleId && lessonsList.length > 0) {
|
||||||
|
expandedState[module.id] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`⚠️ No lessons found for module ${module.id}`)
|
||||||
|
lessonsData[module.id] = []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error fetching lessons for module ${module.id}:`, error)
|
||||||
|
lessonsData[module.id] = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLessons(lessonsData)
|
||||||
|
setExpandedModules(expandedState)
|
||||||
|
|
||||||
|
// Auto-select first lesson if available
|
||||||
|
if (!selectedModuleId && modulesList.length > 0) {
|
||||||
|
const firstModule = modulesList[0]
|
||||||
|
const firstModuleLessons = lessonsData[firstModule.id] || []
|
||||||
|
|
||||||
|
setSelectedModuleId(firstModule.id)
|
||||||
|
if (firstModuleLessons.length > 0) {
|
||||||
|
setSelectedLessonId(firstModuleLessons[0].id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper: embed URL
|
// Helper: embed URL
|
||||||
function getEmbedUrl(url?: string): string | undefined {
|
function getEmbedUrl(url?: string): string | undefined {
|
||||||
@@ -75,182 +262,475 @@ export default function CoursePage() {
|
|||||||
if (match && match[1]) {
|
if (match && match[1]) {
|
||||||
return `https://www.youtube.com/embed/${match[1]}?rel=0&modestbranding=1`
|
return `https://www.youtube.com/embed/${match[1]}?rel=0&modestbranding=1`
|
||||||
}
|
}
|
||||||
// fallback (could already be an embed url or another provider)
|
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
const modules = course?.modules || []
|
const toggleModule = (moduleId: string) => {
|
||||||
// Pick first non-empty for fallback if nothing selected
|
setExpandedModules(prev => ({
|
||||||
const selModIdx = modules.length > 0 ? selectedModuleIdx : 0
|
...prev,
|
||||||
const lessons = modules.length > 0 ? modules[selModIdx]?.lessons : []
|
[moduleId]: !prev[moduleId]
|
||||||
const selLesIdx = lessons.length > 0 ? selectedLessonIdx : 0
|
}))
|
||||||
const currentLesson = lessons.length > 0 ? lessons[selLesIdx] : undefined
|
|
||||||
|
|
||||||
// for navigation
|
|
||||||
const isEnd =
|
|
||||||
modules.length > 0 &&
|
|
||||||
selModIdx === modules.length - 1 &&
|
|
||||||
lessons.length > 0 &&
|
|
||||||
selLesIdx === lessons.length - 1
|
|
||||||
|
|
||||||
function prev() {
|
|
||||||
if (selLesIdx > 0) setSelectedLessonIdx(selLesIdx - 1)
|
|
||||||
else if (selModIdx > 0) {
|
|
||||||
const prevLessons = modules[selModIdx - 1].lessons
|
|
||||||
setSelectedModuleIdx(selModIdx - 1)
|
|
||||||
setSelectedLessonIdx(Math.max(prevLessons.length - 1, 0))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectLesson = (moduleId: string, lessonId: string) => {
|
||||||
|
setSelectedModuleId(moduleId)
|
||||||
|
setSelectedLessonId(lessonId)
|
||||||
|
// Auto-expand the module when lesson is selected
|
||||||
|
setExpandedModules(prev => ({
|
||||||
|
...prev,
|
||||||
|
[moduleId]: true
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
function next() {
|
|
||||||
if (lessons.length && selLesIdx < lessons.length - 1) setSelectedLessonIdx(selLesIdx + 1)
|
const getCurrentLesson = (): Lesson | null => {
|
||||||
else if (selModIdx < modules.length - 1) {
|
if (!selectedModuleId || !selectedLessonId) return null
|
||||||
setSelectedModuleIdx(selModIdx + 1)
|
const moduleLessons = lessons[selectedModuleId] || []
|
||||||
setSelectedLessonIdx(0)
|
return moduleLessons.find(lesson => lesson.id === selectedLessonId) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllLessons = (): Lesson[] => {
|
||||||
|
const allLessons: Lesson[] = []
|
||||||
|
modules.forEach(module => {
|
||||||
|
const moduleLessons = lessons[module.id] || []
|
||||||
|
allLessons.push(...moduleLessons)
|
||||||
|
})
|
||||||
|
return allLessons
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigateLesson = (direction: 'prev' | 'next') => {
|
||||||
|
const allLessons = getAllLessons()
|
||||||
|
const currentIndex = allLessons.findIndex(lesson => lesson.id === selectedLessonId)
|
||||||
|
|
||||||
|
if (direction === 'prev' && currentIndex > 0) {
|
||||||
|
const prevLesson = allLessons[currentIndex - 1]
|
||||||
|
selectLesson(prevLesson.module_id, prevLesson.id)
|
||||||
|
} else if (direction === 'next' && currentIndex < allLessons.length - 1) {
|
||||||
|
const nextLesson = allLessons[currentIndex + 1]
|
||||||
|
selectLesson(nextLesson.module_id, nextLesson.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function markComplete() {
|
const isFirstLesson = () => {
|
||||||
|
const allLessons = getAllLessons()
|
||||||
|
return allLessons.length > 0 && allLessons[0].id === selectedLessonId
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLastLesson = () => {
|
||||||
|
const allLessons = getAllLessons()
|
||||||
|
return allLessons.length > 0 && allLessons[allLessons.length - 1].id === selectedLessonId
|
||||||
|
}
|
||||||
|
|
||||||
|
const markComplete = () => {
|
||||||
setCompleted(true)
|
setCompleted(true)
|
||||||
toast.success("Course Completed!")
|
toast.success("Course Completed! 🎉")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authLoading || loading) return (
|
const getTotalLessons = () => {
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
return Object.values(lessons).reduce((total, moduleLessons) => total + moduleLessons.length, 0)
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-indigo-700" /><span className="ml-2">Loading course...</span>
|
}
|
||||||
|
|
||||||
|
const currentLesson = getCurrentLesson()
|
||||||
|
|
||||||
|
if (authLoading || loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<Loader2 className="h-12 w-12 animate-spin text-indigo-600 mx-auto mb-4" />
|
||||||
|
<p className="text-lg text-gray-700">Loading course...</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (error) return (
|
if (error) {
|
||||||
<div className="flex items-center justify-center min-h-screen text-red-500">{error}</div>
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<div className="text-center max-w-md mx-auto px-4">
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-lg p-6">
|
||||||
|
<h2 className="text-lg font-semibold text-red-800 mb-2">Error Loading Course</h2>
|
||||||
|
<p className="text-red-600 mb-4">{error}</p>
|
||||||
|
<button
|
||||||
|
onClick={fetchCourseData}
|
||||||
|
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors"
|
||||||
|
>
|
||||||
|
Try Again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
if (!course) return (
|
}
|
||||||
<div className="flex items-center justify-center min-h-screen text-gray-700">Course not found.</div>
|
|
||||||
|
if (!course) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800 mb-2">Course Not Found</h2>
|
||||||
|
<p className="text-gray-600">The course you're looking for doesn't exist.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col md:flex-row gap-6 max-w-7xl mx-auto px-4 py-8">
|
<div className="min-h-screen bg-gray-50">
|
||||||
{/* Sidebar: Always show all modules and lessons */}
|
{/* Header */}
|
||||||
<aside className="w-full md:w-64 bg-white rounded-xl shadow-md p-4 md:sticky md:top-20 h-fit max-h-[80vh] overflow-y-auto mb-4 md:mb-0">
|
<header className="bg-white shadow-sm border-b">
|
||||||
<h2 className="text-lg font-bold mb-4 text-indigo-700">{course.title}</h2>
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<p className="text-xs text-gray-500 mb-5">{course.description}</p>
|
<div className="flex items-center justify-between h-16">
|
||||||
{modules.length === 0 ? (
|
<div className="flex items-center space-x-4">
|
||||||
<div className="text-gray-500 italic py-6">No modules yet for this course.</div>
|
<div className="w-10 h-10 bg-indigo-600 rounded-lg flex items-center justify-center">
|
||||||
) : (
|
<span className="text-white font-bold text-lg">OL</span>
|
||||||
<ul>
|
|
||||||
{modules.map((mod, mIdx) => (
|
|
||||||
<li key={mod.id} className="mb-4">
|
|
||||||
<div
|
|
||||||
className={`font-semibold mb-2 cursor-pointer ${
|
|
||||||
mIdx === selModIdx ? "text-purple-600" : "text-gray-700"
|
|
||||||
}`}
|
|
||||||
onClick={() => { setSelectedModuleIdx(mIdx); setSelectedLessonIdx(0); }}
|
|
||||||
>
|
|
||||||
{mod.title}
|
|
||||||
</div>
|
</div>
|
||||||
<ul className="pl-4 border-l-2 border-gray-100">
|
<div>
|
||||||
{mod.lessons.map((lesson, lIdx) => (
|
<h1 className="text-xl font-bold text-gray-900">{course.title}</h1>
|
||||||
<li
|
<p className="text-sm text-gray-600">by {course.mentor}</p>
|
||||||
key={lesson.id}
|
</div>
|
||||||
className={
|
</div>
|
||||||
`py-1 px-2 rounded mb-1 cursor-pointer text-sm
|
<div className="hidden md:flex items-center space-x-6 text-sm">
|
||||||
${mIdx===selModIdx && lIdx===selLesIdx
|
<div className="flex items-center text-gray-600">
|
||||||
? "bg-indigo-600 text-white"
|
<BookOpen className="w-4 h-4 mr-2" />
|
||||||
: "hover:bg-indigo-100 text-gray-700"}`
|
<span>{modules.length} modules</span>
|
||||||
}
|
</div>
|
||||||
onClick={() => { setSelectedModuleIdx(mIdx); setSelectedLessonIdx(lIdx); }}
|
<div className="flex items-center text-gray-600">
|
||||||
|
<Play className="w-4 h-4 mr-2" />
|
||||||
|
<span>{getTotalLessons()} lessons</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center text-gray-600">
|
||||||
|
<Users className="w-4 h-4 mr-2" />
|
||||||
|
<span>{course.students} students</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<div className="lg:grid lg:grid-cols-12 lg:gap-8">
|
||||||
|
|
||||||
|
{/* Sidebar */}
|
||||||
|
<aside className="lg:col-span-4 xl:col-span-3">
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border p-6 sticky top-8">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">Course Content</h2>
|
||||||
|
|
||||||
|
{/* Debug Info - Enhanced */}
|
||||||
|
<div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
|
<h3 className="text-sm font-semibold text-blue-800 mb-2">🔍 Debug Info:</h3>
|
||||||
|
<div className="text-xs space-y-1 text-blue-700">
|
||||||
|
<p><strong>Course ID:</strong> {courseId}</p>
|
||||||
|
<p><strong>Modules Loaded:</strong> {modules.length}</p>
|
||||||
|
<p><strong>Total Lessons:</strong> {getTotalLessons()}</p>
|
||||||
|
<p><strong>Modules Loading:</strong> {modulesLoading ? 'Yes' : 'No'}</p>
|
||||||
|
<p><strong>Selected Module:</strong> {selectedModuleId || 'None'}</p>
|
||||||
|
<p><strong>Selected Lesson:</strong> {currentLesson?.title || 'None'}</p>
|
||||||
|
<p><strong>Expanded Modules:</strong> {Object.keys(expandedModules).length}</p>
|
||||||
|
</div>
|
||||||
|
{modules.length > 0 && (
|
||||||
|
<details className="mt-2">
|
||||||
|
<summary className="text-xs cursor-pointer text-blue-600">Show Raw Data</summary>
|
||||||
|
<pre className="text-xs mt-2 p-2 bg-gray-100 rounded overflow-auto max-h-32">
|
||||||
|
{JSON.stringify({ modules, lessons }, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Loading State */}
|
||||||
|
{modulesLoading && (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-indigo-600 mx-auto mb-2" />
|
||||||
|
<p className="text-sm text-gray-600">Loading modules...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* No Modules State */}
|
||||||
|
{!modulesLoading && modules.length === 0 && (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||||
|
<h3 className="text-sm font-semibold text-yellow-800 mb-2">No Modules Found</h3>
|
||||||
|
<p className="text-xs text-yellow-700 mb-3">
|
||||||
|
This could mean:
|
||||||
|
</p>
|
||||||
|
<ul className="text-xs text-yellow-700 text-left space-y-1">
|
||||||
|
<li>• No modules created for this course yet</li>
|
||||||
|
<li>• API endpoint issues</li>
|
||||||
|
<li>• Course ID mismatch</li>
|
||||||
|
</ul>
|
||||||
|
<button
|
||||||
|
onClick={() => fetchModulesAndLessons(courseId)}
|
||||||
|
className="mt-3 px-3 py-1 bg-yellow-600 text-white text-xs rounded hover:bg-yellow-700"
|
||||||
>
|
>
|
||||||
{lesson.title}
|
Retry Loading Modules
|
||||||
</li>
|
</button>
|
||||||
))}
|
</div>
|
||||||
{mod.lessons.length === 0 && (
|
</div>
|
||||||
<li className="text-xs text-gray-400 pl-1 py-1">No lessons</li>
|
|
||||||
)}
|
)}
|
||||||
</ul>
|
|
||||||
</li>
|
{/* Modules List */}
|
||||||
))}
|
{!modulesLoading && modules.length > 0 && (
|
||||||
</ul>
|
<div className="space-y-2">
|
||||||
|
{modules.map((module, index) => (
|
||||||
|
<div key={module.id} className="border border-gray-200 rounded-lg overflow-hidden">
|
||||||
|
{/* Module Header */}
|
||||||
|
<button
|
||||||
|
onClick={() => toggleModule(module.id)}
|
||||||
|
className={`w-full px-4 py-3 text-left hover:bg-gray-50 flex items-center justify-between transition-colors ${
|
||||||
|
selectedModuleId === module.id ? 'bg-indigo-50 border-indigo-200' : 'bg-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="flex-shrink-0 w-6 h-6 bg-indigo-100 text-indigo-800 rounded-full flex items-center justify-center text-xs font-semibold">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
<h3 className="font-medium text-sm text-gray-900 truncate">{module.title}</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-1 ml-8">
|
||||||
|
{lessons[module.id]?.length || 0} lessons
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0 ml-2">
|
||||||
|
{expandedModules[module.id] ? (
|
||||||
|
<ChevronDown className="w-4 h-4 text-gray-400" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="w-4 h-4 text-gray-400" />
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Lessons */}
|
||||||
|
{expandedModules[module.id] && (
|
||||||
|
<div className="bg-gray-50 border-t border-gray-200">
|
||||||
|
{lessons[module.id] && lessons[module.id].length > 0 ? (
|
||||||
|
lessons[module.id].map((lesson, lessonIndex) => (
|
||||||
|
<button
|
||||||
|
key={lesson.id}
|
||||||
|
onClick={() => selectLesson(module.id, lesson.id)}
|
||||||
|
className={`w-full px-6 py-3 text-left hover:bg-gray-100 transition-colors border-l-4 ${
|
||||||
|
selectedLessonId === lesson.id
|
||||||
|
? 'border-indigo-500 bg-indigo-50 text-indigo-900'
|
||||||
|
: 'border-transparent text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className={`flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center text-xs ${
|
||||||
|
selectedLessonId === lesson.id
|
||||||
|
? 'bg-indigo-500 text-white'
|
||||||
|
: 'bg-gray-300 text-gray-600'
|
||||||
|
}`}>
|
||||||
|
<Play className="w-2.5 h-2.5" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="font-medium text-sm truncate">{lesson.title}</p>
|
||||||
|
{lesson.duration && (
|
||||||
|
<p className={`text-xs flex items-center mt-1 ${
|
||||||
|
selectedLessonId === lesson.id ? 'text-indigo-600' : 'text-gray-500'
|
||||||
|
}`}>
|
||||||
|
<Clock className="w-3 h-3 mr-1" />
|
||||||
|
{lesson.duration}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="px-6 py-4 text-center">
|
||||||
|
<p className="text-xs text-gray-500">No lessons in this module</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Main: show lesson or course video/desc/mark as read */}
|
{/* Main Content */}
|
||||||
<main className="flex-1 bg-white rounded-xl shadow-md p-6 min-h-80 max-w-2xl mx-auto">
|
<main className="mt-8 lg:mt-0 lg:col-span-8 xl:col-span-9">
|
||||||
{modules.length > 0 && lessons.length > 0 && currentLesson ? (
|
<div className="bg-white rounded-xl shadow-sm border overflow-hidden">
|
||||||
|
{currentLesson ? (
|
||||||
<>
|
<>
|
||||||
<h2 className="text-2xl font-bold mb-2">{currentLesson.title}</h2>
|
{/* Video Player */}
|
||||||
{currentLesson.video_url && (
|
{(currentLesson.embed_url || currentLesson.video_url) && (
|
||||||
<div className="aspect-video rounded overflow-hidden my-4 shadow-lg">
|
<div className="aspect-video bg-black">
|
||||||
<iframe
|
<iframe
|
||||||
src={getEmbedUrl(currentLesson.video_url)}
|
src={getEmbedUrl(currentLesson.embed_url || currentLesson.video_url)}
|
||||||
title={currentLesson.title}
|
title={currentLesson.title}
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
className="w-full h-full"
|
className="w-full h-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{currentLesson.description && (
|
|
||||||
<div className="text-gray-700 mb-6">{currentLesson.description}</div>
|
{/* Lesson Content */}
|
||||||
|
<div className="p-8">
|
||||||
|
{/* Lesson Header */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex items-center text-sm text-gray-500 mb-2">
|
||||||
|
<User className="w-4 h-4 mr-1" />
|
||||||
|
<span>by {course.mentor}</span>
|
||||||
|
<span className="mx-2">•</span>
|
||||||
|
<span className="bg-indigo-100 text-indigo-800 px-2 py-1 rounded-full text-xs font-medium">
|
||||||
|
{course.difficulty}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">{currentLesson.title}</h1>
|
||||||
|
{currentLesson.duration && (
|
||||||
|
<div className="flex items-center text-sm text-gray-600">
|
||||||
|
<Clock className="w-4 h-4 mr-1" />
|
||||||
|
<span>{currentLesson.duration}</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Navigation / mark as read */}
|
</div>
|
||||||
<div className="flex justify-between gap-2">
|
|
||||||
|
{/* Lesson Description */}
|
||||||
|
{currentLesson.description && (
|
||||||
|
<div className="prose max-w-none mb-8">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 mb-4">About this lesson</h2>
|
||||||
|
<div className="text-gray-700 leading-relaxed whitespace-pre-line bg-gray-50 p-4 rounded-lg">
|
||||||
|
{currentLesson.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Lesson Content */}
|
||||||
|
{currentLesson.content && (
|
||||||
|
<div className="prose max-w-none mb-8">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 mb-4">Lesson Content</h2>
|
||||||
|
<div className="text-gray-700 leading-relaxed whitespace-pre-line bg-gray-50 p-4 rounded-lg">
|
||||||
|
{currentLesson.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<div className="flex justify-between items-center pt-8 border-t border-gray-200">
|
||||||
<button
|
<button
|
||||||
className="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300 disabled:opacity-60"
|
onClick={() => navigateLesson('prev')}
|
||||||
onClick={prev}
|
disabled={isFirstLesson()}
|
||||||
disabled={selModIdx===0 && selLesIdx===0}
|
className="px-6 py-3 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
|
||||||
>
|
>
|
||||||
Previous
|
← Previous Lesson
|
||||||
</button>
|
</button>
|
||||||
{!isEnd ? (
|
|
||||||
<button className="px-4 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700" onClick={next}>
|
{!isLastLesson() ? (
|
||||||
Next
|
<button
|
||||||
|
onClick={() => navigateLesson('next')}
|
||||||
|
className="px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors font-medium"
|
||||||
|
>
|
||||||
|
Next Lesson →
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
className={`px-4 py-2 rounded font-semibold ${
|
|
||||||
completed
|
|
||||||
? "bg-green-600 text-white"
|
|
||||||
: "bg-purple-600 text-white hover:bg-purple-700"
|
|
||||||
}`}
|
|
||||||
onClick={markComplete}
|
onClick={markComplete}
|
||||||
disabled={completed}
|
disabled={completed}
|
||||||
|
className={`px-6 py-3 rounded-lg font-medium transition-colors ${
|
||||||
|
completed
|
||||||
|
? "bg-green-600 text-white cursor-not-allowed"
|
||||||
|
: "bg-purple-600 text-white hover:bg-purple-700"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{completed ? "Course Completed ✓" : "Mark as Read"}
|
{completed ? "✓ Course Completed" : "Mark as Complete"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Completion Message */}
|
||||||
{completed && (
|
{completed && (
|
||||||
<div className="mt-6 bg-green-50 border border-green-300 p-4 rounded text-green-700 text-center font-bold shadow">
|
<div className="mt-8 bg-green-50 border border-green-200 rounded-lg p-6 text-center">
|
||||||
🎉 Course Completed! Certificate coming soon.
|
<div className="text-green-700">
|
||||||
|
<div className="text-4xl mb-2">🎉</div>
|
||||||
|
<h3 className="text-xl font-bold mb-2">Congratulations!</h3>
|
||||||
|
<p>You have successfully completed this course. Certificate coming soon!</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
) :
|
) : (
|
||||||
// Course has no modules or no lessons
|
/* Course Overview */
|
||||||
(
|
<div className="p-8 text-center">
|
||||||
<div>
|
<div className="max-w-3xl mx-auto">
|
||||||
<h2 className="text-2xl font-bold mb-3">{course.title}</h2>
|
<h1 className="text-4xl font-bold text-gray-900 mb-4">{course.title}</h1>
|
||||||
<p className="mb-6 text-gray-700">{course.description}</p>
|
|
||||||
{(course.embed_url || course.video_url) ? (
|
<div className="flex items-center justify-center space-x-4 mb-6 text-sm">
|
||||||
<div className="aspect-video rounded-lg overflow-hidden my-5 shadow-lg">
|
<div className="flex items-center text-gray-600">
|
||||||
|
<User className="w-4 h-4 mr-1" />
|
||||||
|
<span>by {course.mentor}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Star className="w-4 h-4 text-yellow-400 mr-1" />
|
||||||
|
<span className="text-gray-600">4.8</span>
|
||||||
|
</div>
|
||||||
|
<span className="bg-indigo-100 text-indigo-800 px-3 py-1 rounded-full text-sm font-medium">
|
||||||
|
{course.difficulty}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-lg text-gray-700 mb-8 leading-relaxed">{course.description}</p>
|
||||||
|
|
||||||
|
{/* Course Stats */}
|
||||||
|
<div className="grid grid-cols-3 gap-8 mb-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-3xl font-bold text-indigo-600 mb-1">{modules.length}</div>
|
||||||
|
<div className="text-sm text-gray-600">Modules</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-3xl font-bold text-indigo-600 mb-1">{getTotalLessons()}</div>
|
||||||
|
<div className="text-sm text-gray-600">Lessons</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-3xl font-bold text-indigo-600 mb-1">{course.students}</div>
|
||||||
|
<div className="text-sm text-gray-600">Students</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Course Intro Video */}
|
||||||
|
{(course.embed_url || course.video_url) && (
|
||||||
|
<div className="aspect-video rounded-xl overflow-hidden mb-8 shadow-lg bg-black">
|
||||||
<iframe
|
<iframe
|
||||||
src={getEmbedUrl(course.embed_url || course.video_url)}
|
src={getEmbedUrl(course.embed_url || course.video_url)}
|
||||||
title={`Video for ${course.title}`}
|
title={course.title}
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
className="w-full h-full"
|
className="w-full h-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{getTotalLessons() > 0 ? (
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
Ready to start learning? Select a lesson from the course content to begin your journey.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const firstModule = modules[0]
|
||||||
|
const firstLessons = lessons[firstModule?.id] || []
|
||||||
|
if (firstLessons.length > 0) {
|
||||||
|
selectLesson(firstModule.id, firstLessons[0].id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="px-8 py-4 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 font-semibold text-lg transition-colors"
|
||||||
|
>
|
||||||
|
Start Learning
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-gray-400 italic mb-4">No video available for this course yet.</div>
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-6">
|
||||||
|
<h3 className="text-lg font-semibold text-yellow-800 mb-2">Coming Soon</h3>
|
||||||
|
<p className="text-yellow-700">Lessons are being prepared for this course. Check back soon!</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mt-8 text-gray-500 text-center">
|
|
||||||
No modules or lessons yet.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user