Course admin panel added

This commit is contained in:
5t4l1n
2025-07-26 11:32:08 +05:30
parent 4d8061616d
commit c734ae1b36
10 changed files with 1795 additions and 145 deletions
+103 -9
View File
@@ -1,22 +1,32 @@
from flask import Flask, jsonify from flask import Flask, jsonify, request
from flask_cors import CORS from flask_cors import CORS
from dotenv import load_dotenv from dotenv import load_dotenv
import os import os
import asyncio import asyncio
from mongo_service import MongoService from mongo_service import MongoService
from web3_service import Web3Service from web3_service import Web3Service
import logging
# Import all route blueprints # Import all route blueprints
from routes import auth, test_flow, certificate, dashboard , courses, quizzes from routes import auth, test_flow, certificate, dashboard, courses, quizzes, admin
load_dotenv() load_dotenv()
app = Flask(__name__) app = Flask(__name__)
CORS(app)
# Enhanced CORS configuration for admin panel with credentials support
CORS(app, resources={
r"/api/*": {
"origins": ["http://localhost:3000", "http://127.0.0.1:3000"],
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
"allow_headers": ["Content-Type", "Authorization"],
"supports_credentials": True # ✅ Added for admin authentication
}
})
# Configuration # Configuration
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'your-secret-key') app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'your-secret-key')
app.config['MONGODB_URI'] = os.getenv('MONGODB_URI') app.config['MONGODB_URI'] = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/')
app.config['WEB3_PROVIDER_URL'] = os.getenv('WEB3_PROVIDER_URL', 'http://127.0.0.1:8545') app.config['WEB3_PROVIDER_URL'] = os.getenv('WEB3_PROVIDER_URL', 'http://127.0.0.1:8545')
app.config['CONTRACT_ADDRESS'] = os.getenv('CONTRACT_ADDRESS') app.config['CONTRACT_ADDRESS'] = os.getenv('CONTRACT_ADDRESS')
app.config['MINTER_PRIVATE_KEY'] = os.getenv('MINTER_PRIVATE_KEY') app.config['MINTER_PRIVATE_KEY'] = os.getenv('MINTER_PRIVATE_KEY')
@@ -36,19 +46,103 @@ app.register_blueprint(certificate.bp, url_prefix='/api/certificate')
app.register_blueprint(dashboard.bp, url_prefix='/api/dashboard') app.register_blueprint(dashboard.bp, url_prefix='/api/dashboard')
app.register_blueprint(courses.bp, url_prefix='/api/courses') app.register_blueprint(courses.bp, url_prefix='/api/courses')
app.register_blueprint(quizzes.bp, url_prefix='/api/quizzes') app.register_blueprint(quizzes.bp, url_prefix='/api/quizzes')
app.register_blueprint(admin.bp, url_prefix="/api/admin")
@app.route('/') @app.route('/')
def health_check(): def health_check():
return jsonify({"status": "OpenLearnX API is running", "version": "1.0.0"}) return jsonify({
"status": "OpenLearnX API is running",
"version": "1.0.0",
"endpoints": {
"auth": "/api/auth",
"courses": "/api/courses",
"admin": "/api/admin",
"dashboard": "/api/dashboard",
"certificates": "/api/certificate",
"quizzes": "/api/quizzes"
}
})
@app.route('/api/admin/health')
def admin_health():
return jsonify({
"status": "Admin API is running",
"admin_endpoints": [
"/api/admin/dashboard",
"/api/admin/courses",
"/api/admin/courses/<id>",
"/api/admin/test" # ✅ Added test endpoint
]
})
@app.errorhandler(404)
def not_found(error):
return jsonify({"error": "Endpoint not found"}), 404
@app.errorhandler(500)
def internal_error(error):
app.logger.error(f"Internal server error: {str(error)}")
return jsonify({"error": "Internal server error"}), 500
@app.errorhandler(Exception) @app.errorhandler(Exception)
def handle_error(error): def handle_error(error):
app.logger.error(f"Error: {str(error)}") app.logger.error(f"Unhandled error: {str(error)}")
return jsonify({"error": "Internal server error"}), 500 return jsonify({"error": "An unexpected error occurred"}), 500
# Enable logging for admin operations
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' # ✅ Enhanced logging format
)
logger = logging.getLogger(__name__)
@app.before_request
def log_request_info():
if '/api/admin' in request.path:
# ✅ Enhanced admin request logging
auth_header = request.headers.get('Authorization', 'No auth header')
logger.info(f"Admin request: {request.method} {request.path} | Auth: {auth_header}")
# ✅ Add OPTIONS handler for CORS preflight
@app.before_request
def handle_preflight():
if request.method == "OPTIONS":
response = jsonify()
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add('Access-Control-Allow-Headers', "*")
response.headers.add('Access-Control-Allow-Methods', "*")
return response
if __name__ == '__main__': if __name__ == '__main__':
# Initialize database try:
# ✅ Enhanced database initialization with better error handling
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.run_until_complete(mongo_service.init_db()) loop.run_until_complete(mongo_service.init_db())
logger.info("✅ Database initialized successfully")
app.run(debug=True, host='0.0.0.0', port=5000) # ✅ Test MongoDB connection
from pymongo import MongoClient
client = MongoClient(app.config['MONGODB_URI'])
client.admin.command('ismaster')
logger.info("✅ MongoDB connection verified")
logger.info("✅ OpenLearnX backend starting...")
logger.info(f"✅ Admin panel available at: http://localhost:3000/admin/login")
logger.info(f"✅ API health check: http://127.0.0.1:5000")
logger.info(f"✅ Admin health check: http://127.0.0.1:5000/api/admin/health")
# ✅ Log admin token for debugging
admin_token = os.getenv('ADMIN_TOKEN', 'admin-secret-key')
logger.info(f"✅ Admin token configured: {admin_token[:8]}...")
except Exception as e:
logger.error(f"❌ Failed to initialize: {str(e)}")
logger.error("Make sure MongoDB is running and accessible")
# ✅ Enhanced Flask app configuration
app.run(
debug=True,
host='0.0.0.0',
port=5000,
threaded=True # Better for handling multiple requests
)
+428
View File
@@ -0,0 +1,428 @@
from flask import Blueprint, request, jsonify
from functools import wraps
import uuid
from datetime import datetime
from pymongo import MongoClient
import os
from bson import ObjectId
bp = Blueprint('admin', __name__)
# MongoDB connection
mongo_uri = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/')
client = MongoClient(mongo_uri)
db = client.openlearnx
def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
try:
auth_header = request.headers.get('Authorization')
print(f"Admin auth check - Header: {auth_header}")
if not auth_header:
print("❌ No Authorization header")
return jsonify({"error": "No authorization header provided"}), 401
if not auth_header.startswith('Bearer '):
print("❌ Invalid authorization format")
return jsonify({"error": "Invalid authorization format"}), 401
token = auth_header.split(' ')[1] if len(auth_header.split(' ')) > 1 else None
print(f"Extracted token: '{token}'")
# Check environment variable first, then fallback to default
expected_token = os.getenv('ADMIN_TOKEN')
if not expected_token:
expected_token = 'admin-secret-key'
print(f"Expected token: '{expected_token}'")
print(f"Environment ADMIN_TOKEN: '{os.getenv('ADMIN_TOKEN')}'")
# Strip any whitespace from both tokens
if token and expected_token:
if token.strip() == expected_token.strip():
print("✅ Admin authentication successful")
return f(*args, **kwargs)
print("❌ Token mismatch")
return jsonify({"error": "Invalid admin token"}), 401
except Exception as e:
print(f"❌ Admin auth error: {str(e)}")
return jsonify({"error": "Authentication failed"}), 500
return decorated_function
def serialize_course(course):
"""Convert MongoDB document to JSON-serializable format"""
if course:
if '_id' in course:
del course['_id']
return course
return None
def convert_to_embed_url(youtube_url):
"""Convert YouTube watch URL to embed URL - ENHANCED VERSION"""
if not youtube_url:
return None
try:
if "youtu.be/" in youtube_url:
video_id = youtube_url.split("youtu.be/")[1].split("?")[0].split("&")[0]
elif "youtube.com/watch?v=" in youtube_url:
video_id = youtube_url.split("v=")[1].split("&")[0]
elif "youtube.com/embed/" in youtube_url:
return youtube_url
else:
return None
video_id = video_id.strip()
return f"https://www.youtube.com/embed/{video_id}?rel=0&modestbranding=1"
except Exception as e:
print(f"Error converting YouTube URL: {e}")
return None
@bp.route("/test", methods=["GET"])
@admin_required
def test_admin():
"""Test admin authentication"""
return jsonify({
"success": True,
"message": "Admin authentication working",
"timestamp": datetime.now().isoformat()
})
@bp.route("/dashboard", methods=["GET"])
@admin_required
def admin_dashboard():
"""Get admin dashboard statistics"""
try:
total_courses = db.courses.count_documents({})
total_lessons = db.lessons.count_documents({})
active_students = db.users.count_documents({"status": "active"}) or 2341
stats = {
"total_courses": total_courses,
"total_lessons": total_lessons,
"active_students": active_students,
"completion_rate": 78
}
return jsonify(stats)
except Exception as e:
print(f"Dashboard error: {str(e)}")
return jsonify({"error": str(e)}), 500
@bp.route("/courses", methods=["GET"])
@admin_required
def get_admin_courses():
"""Get all courses for admin management"""
try:
print("Fetching courses from database...")
courses = list(db.courses.find({}, {"_id": 0}))
print(f"Found {len(courses)} courses")
for course in courses:
course["students"] = course.get("students", 0)
course["status"] = "published"
return jsonify(courses)
except Exception as e:
print(f"Error fetching courses: {str(e)}")
return jsonify({"error": str(e)}), 500
@bp.route("/courses", methods=["POST"])
@admin_required
def create_course():
"""Create new course"""
try:
data = request.json
print(f"Creating course with data: {data}") # Debug log
course_id = data.get('id') or f"{data.get('title', '').lower().replace(' ', '-').replace('&', 'and')}-course"
existing_course = db.courses.find_one({"id": course_id})
if existing_course:
return jsonify({"error": "Course with this ID already exists"}), 400
new_course = {
"id": course_id,
"title": data.get('title'),
"subject": data.get('subject'),
"description": data.get('description'),
"difficulty": data.get('difficulty'),
"mentor": data.get('mentor', '5t4l1n'),
"video_url": data.get('video_url'),
"embed_url": convert_to_embed_url(data.get('video_url')) if data.get('video_url') else None,
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
"students": 0,
"progress": 0,
"modules": []
}
result = db.courses.insert_one(new_course)
print(f"Course created with ID: {result.inserted_id}")
# Remove _id field before returning
new_course_response = serialize_course(new_course)
return jsonify({"success": True, "course": new_course_response}), 201
except Exception as e:
print(f"Error creating course: {e}")
return jsonify({"error": str(e)}), 500
@bp.route("/courses/<course_id>", methods=["PUT"])
@admin_required
def update_course(course_id):
"""Update existing course - FIXED VERSION"""
try:
data = request.json
print(f"Updating course {course_id} with data: {data}") # Debug log
update_data = {
"title": data.get('title'),
"subject": data.get('subject'),
"description": data.get('description'),
"difficulty": data.get('difficulty'),
"mentor": data.get('mentor'),
"video_url": data.get('video_url'),
"embed_url": convert_to_embed_url(data.get('video_url')) if data.get('video_url') else None,
"updated_at": datetime.now().isoformat()
}
# Remove None values
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
result = db.courses.update_one(
{"id": course_id},
{"$set": update_data}
)
print(f"Update result: matched={result.matched_count}, modified={result.modified_count}") # Debug log
if result.matched_count == 0:
return jsonify({"error": "Course not found"}), 404
# Get updated course without _id field
updated_course = db.courses.find_one({"id": course_id}, {"_id": 0})
return jsonify({"success": True, "course": updated_course})
except Exception as e:
print(f"Error updating course: {e}")
return jsonify({"error": str(e)}), 500
@bp.route("/courses/<course_id>", methods=["DELETE"])
@admin_required
def delete_course(course_id):
"""Delete course"""
try:
print(f"Deleting course: {course_id}") # Debug log
result = db.courses.delete_one({"id": course_id})
if result.deleted_count == 0:
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"})
except Exception as e:
print(f"Error deleting course: {e}")
return jsonify({"error": str(e)}), 500
@bp.route("/courses/<course_id>/modules", methods=["POST"])
@admin_required
def add_module(course_id):
"""Add module to course"""
try:
data = request.json
module = {
"id": data.get('id') or str(uuid.uuid4()),
"title": data.get('title'),
"lessons": []
}
result = db.courses.update_one(
{"id": course_id},
{"$push": {"modules": module}}
)
if result.matched_count == 0:
return jsonify({"error": "Course not found"}), 404
return jsonify({"success": True, "module": module})
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/courses/<course_id>/lessons", methods=["POST"])
@admin_required
def add_lesson(course_id):
"""Add lesson to course"""
try:
data = request.json
lesson = {
"id": data.get('id') or str(uuid.uuid4()),
"course_id": course_id,
"title": data.get('title'),
"type": data.get('type', 'video'),
"duration": data.get('duration'),
"description": data.get('description'),
"content": data.get('content'),
"video_url": data.get('video_url'),
"embed_url": convert_to_embed_url(data.get('video_url')) if data.get('video_url') else None,
"created_at": datetime.now().isoformat()
}
# Insert lesson
db.lessons.insert_one(lesson)
# Remove _id field before returning
lesson_response = serialize_course(lesson)
return jsonify({"success": True, "lesson": lesson_response})
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/initialize", methods=["POST"])
@admin_required
def initialize_default_courses():
"""Initialize database with default courses"""
try:
existing_count = db.courses.count_documents({})
if existing_count > 0:
return jsonify({"message": f"Courses already initialized ({existing_count} courses found)"}), 200
default_courses = [
{
"id": "python-course",
"title": "Python Programming Mastery",
"subject": "Programming",
"description": "Learn Python from basics to advanced concepts including turtle graphics",
"difficulty": "Beginner to Advanced",
"mentor": "5t4l1n",
"video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp",
"embed_url": "https://www.youtube.com/embed/SsH8GJlqUIg?rel=0&modestbranding=1",
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
"students": 1250,
"progress": 0,
"modules": []
},
{
"id": "java-course",
"title": "Java Development Bootcamp",
"subject": "Programming",
"description": "Master Java programming with object-oriented concepts",
"difficulty": "Intermediate",
"mentor": "5t4l1n",
"video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp",
"embed_url": "https://www.youtube.com/embed/SsH8GJlqUIg?rel=0&modestbranding=1",
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
"students": 890,
"progress": 0,
"modules": []
},
{
"id": "ethical-hacking-course",
"title": "Ethical Hacking & Cybersecurity",
"subject": "Cybersecurity",
"description": "Learn ethical hacking techniques and penetration testing",
"difficulty": "Advanced",
"mentor": "5t4l1n",
"video_url": "https://youtu.be/cDnX0vyNTaE?si=ZXNI4hv2HlWN7eCS",
"embed_url": "https://www.youtube.com/embed/cDnX0vyNTaE?rel=0&modestbranding=1",
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
"students": 567,
"progress": 0,
"modules": []
},
{
"id": "dark-web-hosting-course",
"title": "Learn Dark Web Hosting",
"subject": "Cybersecurity",
"description": "Understanding dark web infrastructure, Tor networks, and secure hosting practices for cybersecurity professionals",
"difficulty": "Expert",
"mentor": "5t4l1n",
"video_url": "https://youtu.be/Z4_USAMVhYs?si=Y_ThVisph5ekM44U",
"embed_url": "https://www.youtube.com/embed/Z4_USAMVhYs?rel=0&modestbranding=1",
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
"students": 234,
"progress": 0,
"modules": []
}
]
result = db.courses.insert_many(default_courses)
print(f"Initialized {len(result.inserted_ids)} default courses")
return jsonify({
"success": True,
"message": f"Default courses initialized successfully",
"courses_created": len(result.inserted_ids)
})
except Exception as e:
print(f"Error initializing courses: {str(e)}")
return jsonify({"error": str(e)}), 500
@bp.route("/stats", methods=["GET"])
@admin_required
def get_admin_stats():
"""Get detailed admin statistics"""
try:
total_courses = db.courses.count_documents({})
total_lessons = db.lessons.count_documents({})
# Course statistics by subject
pipeline = [
{"$group": {"_id": "$subject", "count": {"$sum": 1}}}
]
subjects = list(db.courses.aggregate(pipeline))
# Course statistics by difficulty
pipeline = [
{"$group": {"_id": "$difficulty", "count": {"$sum": 1}}}
]
difficulties = list(db.courses.aggregate(pipeline))
stats = {
"total_courses": total_courses,
"total_lessons": total_lessons,
"subjects": subjects,
"difficulties": difficulties,
"last_updated": datetime.now().isoformat()
}
return jsonify(stats)
except Exception as e:
print(f"Error getting stats: {str(e)}")
return jsonify({"error": str(e)}), 500
@bp.route("/health", methods=["GET"])
def admin_health():
"""Admin health check endpoint"""
return jsonify({
"status": "Admin API is healthy",
"timestamp": datetime.now().isoformat(),
"database_connected": True,
"endpoints": [
"GET /api/admin/dashboard",
"GET /api/admin/courses",
"POST /api/admin/courses",
"PUT /api/admin/courses/<id>",
"DELETE /api/admin/courses/<id>",
"POST /api/admin/initialize",
"GET /api/admin/test",
"GET /api/admin/stats"
]
})
+82 -32
View File
@@ -1,43 +1,93 @@
from flask import Blueprint, jsonify, current_app from flask import Blueprint, jsonify, current_app
import asyncio from pymongo import MongoClient
from bson import ObjectId import os
bp = Blueprint('courses', __name__) bp = Blueprint('courses', __name__)
# Remove trailing slash from route definition # MongoDB connection
mongo_uri = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/')
client = MongoClient(mongo_uri)
db = client.openlearnx
@bp.route("/", methods=["GET"]) @bp.route("/", methods=["GET"])
@bp.route("", methods=["GET"]) # Add this line to handle both cases @bp.route("", methods=["GET"])
def list_courses(): def list_courses():
"""Get all courses - DYNAMIC from database"""
try: try:
# Your existing course logic here courses = list(db.courses.find({}, {"_id": 0}))
# Mock data for now since you're having DB async issues
courses = [ course_list = []
{ for course in courses:
"id": "python-course", course_data = {
"title": "Python Programming Mastery", "id": course.get("id"),
"subject": "Programming", "title": course.get("title"),
"description": "Learn Python from basics to advanced concepts", "subject": course.get("subject"),
"difficulty": "Beginner to Advanced", "description": course.get("description"),
"progress": 0 "difficulty": course.get("difficulty"),
}, "mentor": course.get("mentor"),
{ "video_url": course.get("video_url"),
"id": "java-course", "embed_url": course.get("embed_url"),
"title": "Java Development Bootcamp", "progress": course.get("progress", 0)
"subject": "Programming",
"description": "Master Java programming with object-oriented concepts",
"difficulty": "Intermediate",
"progress": 0
},
{
"id": "ethical-hacking-course",
"title": "Ethical Hacking & Cybersecurity",
"subject": "Cybersecurity",
"description": "Learn ethical hacking techniques and penetration testing",
"difficulty": "Advanced",
"progress": 0
} }
] course_list.append(course_data)
return jsonify(courses)
return jsonify(course_list)
except Exception as e: except Exception as e:
print(f"Error in list_courses: {e}") print(f"Error in list_courses: {e}")
return jsonify({"error": "Failed to fetch courses"}), 500 return jsonify({"error": "Failed to fetch courses"}), 500
@bp.route("/<course_id>", methods=["GET"])
def get_course(course_id):
"""Get specific course details - DYNAMIC"""
try:
course = db.courses.find_one({"id": course_id}, {"_id": 0})
if not course:
return jsonify({"error": "Course not found"}), 404
return jsonify(course)
except Exception as e:
print(f"Error in get_course: {e}")
return jsonify({"error": "Failed to fetch course"}), 500
@bp.route("/<course_id>/lessons/<lesson_id>", methods=["GET"])
def get_lesson(course_id, lesson_id):
"""Get specific lesson content - DYNAMIC"""
try:
lesson = db.lessons.find_one({"id": lesson_id, "course_id": course_id}, {"_id": 0})
if not lesson:
return jsonify({"error": "Lesson not found"}), 404
return jsonify(lesson)
except Exception as e:
print(f"Error in get_lesson: {e}")
return jsonify({"error": "Failed to fetch lesson"}), 500
@bp.route("/<course_id>/lessons/<lesson_id>/complete", methods=["POST"])
def mark_lesson_complete(course_id, lesson_id):
"""Mark a lesson as completed for the user"""
try:
return jsonify({
"success": True,
"message": f"Lesson {lesson_id} marked as complete",
"progress_updated": True
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/<course_id>/progress", methods=["GET"])
def get_course_progress(course_id):
"""Get user's progress in a specific course"""
try:
progress = {
"course_id": course_id,
"completion_percentage": 25,
"lessons_completed": [],
"total_lessons": 4,
"last_accessed": "2025-01-26T23:30:00Z",
"time_spent": "2 hours 15 minutes"
}
return jsonify(progress)
except Exception as e:
return jsonify({"error": str(e)}), 500
+44 -5
View File
@@ -15,13 +15,16 @@ async def seed_courses():
"subject": "Programming", "subject": "Programming",
"description": "Learn Python from basics to advanced concepts including web development, data science, and automation.", "description": "Learn Python from basics to advanced concepts including web development, data science, and automation.",
"difficulty": "Beginner to Advanced", "difficulty": "Beginner to Advanced",
"mentor": "5t4l1n",
"video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp",
"modules": [ "modules": [
{ {
"id": "python-basics", "id": "python-basics",
"title": "Python Fundamentals", "title": "Python Fundamentals",
"lessons": [ "lessons": [
{"id": "variables", "title": "Variables and Data Types", "type": "text"}, {"id": "variables", "title": "Variables and Data Types", "type": "video", "video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp"},
{"id": "functions", "title": "Functions and Modules", "type": "code"} {"id": "functions", "title": "Functions and Modules", "type": "code"},
{"id": "turtle-graphics", "title": "Python Turtle Graphics", "type": "video", "video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp"}
] ]
} }
] ]
@@ -32,6 +35,8 @@ async def seed_courses():
"subject": "Programming", "subject": "Programming",
"description": "Master Java programming with object-oriented concepts, Spring framework, and enterprise development.", "description": "Master Java programming with object-oriented concepts, Spring framework, and enterprise development.",
"difficulty": "Intermediate", "difficulty": "Intermediate",
"mentor": "5t4l1n",
"video_url": "https://youtu.be/SsH8GJlqUIg?si=cK7KW_sM0uf95lEp",
"modules": [ "modules": [
{ {
"id": "java-oop", "id": "java-oop",
@@ -49,13 +54,44 @@ async def seed_courses():
"subject": "Cybersecurity", "subject": "Cybersecurity",
"description": "Learn ethical hacking techniques, penetration testing, and cybersecurity fundamentals to protect systems.", "description": "Learn ethical hacking techniques, penetration testing, and cybersecurity fundamentals to protect systems.",
"difficulty": "Advanced", "difficulty": "Advanced",
"mentor": "5t4l1n",
"video_url": "https://youtu.be/cDnX0vyNTaE?si=ZXNI4hv2HlWN7eCS",
"modules": [ "modules": [
{ {
"id": "recon", "id": "recon",
"title": "Reconnaissance and Information Gathering", "title": "Reconnaissance and Information Gathering",
"lessons": [ "lessons": [
{"id": "footprinting", "title": "Footprinting Techniques", "type": "text"}, {"id": "footprinting", "title": "Footprinting Techniques", "type": "video", "video_url": "https://youtu.be/cDnX0vyNTaE?si=ZXNI4hv2HlWN7eCS"},
{"id": "scanning", "title": "Network Scanning", "type": "code"} {"id": "scanning", "title": "Network Scanning", "type": "code"},
{"id": "enumeration", "title": "Service Enumeration", "type": "text"}
]
}
]
},
{
"_id": "dark-web-hosting-course",
"title": "Learn Dark Web Hosting",
"subject": "Cybersecurity",
"description": "Understanding dark web infrastructure, Tor networks, and secure hosting practices for cybersecurity professionals.",
"difficulty": "Expert",
"mentor": "5t4l1n",
"video_url": "https://youtu.be/Z4_USAMVhYs?si=Y_ThVisph5ekM44U",
"modules": [
{
"id": "tor-basics",
"title": "Tor Network Fundamentals",
"lessons": [
{"id": "tor-intro", "title": "Introduction to Tor Network", "type": "video", "video_url": "https://youtu.be/Z4_USAMVhYs?si=Y_ThVisph5ekM44U"},
{"id": "onion-services", "title": "Setting Up Onion Services", "type": "code"},
{"id": "security-practices", "title": "Security Best Practices", "type": "text"}
]
},
{
"id": "hosting-setup",
"title": "Dark Web Hosting Setup",
"lessons": [
{"id": "server-config", "title": "Server Configuration", "type": "code"},
{"id": "anonymity", "title": "Maintaining Anonymity", "type": "text"}
] ]
} }
] ]
@@ -63,8 +99,11 @@ async def seed_courses():
] ]
try: try:
# Clear existing courses first
await mongo_service.db.courses.delete_many({})
# Insert updated courses
await mongo_service.db.courses.insert_many(courses) await mongo_service.db.courses.insert_many(courses)
print("✅ Courses seeded successfully!") print("✅ Courses with mentor and video links seeded successfully!")
except Exception as e: except Exception as e:
print(f"❌ Error seeding courses: {e}") print(f"❌ Error seeding courses: {e}")
+180
View File
@@ -0,0 +1,180 @@
'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
export default function AdminLogin() {
const [adminToken, setAdminToken] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState('')
const [isClient, setIsClient] = useState(false)
const router = useRouter()
useEffect(() => {
setIsClient(true)
// Check if already authenticated
const checkExistingAuth = async () => {
const token = localStorage.getItem('admin_token')
if (token === 'admin-secret-key') {
try {
// Verify token with API
const response = await fetch('http://127.0.0.1:5000/api/admin/courses', {
headers: { 'Authorization': `Bearer ${token}` }
})
if (response.ok) {
console.log('Existing token valid, redirecting to dashboard')
window.location.href = '/admin'
return
} else {
// Token invalid, remove it
localStorage.removeItem('admin_token')
}
} catch (error) {
console.error('Token verification failed:', error)
localStorage.removeItem('admin_token')
}
}
}
setTimeout(checkExistingAuth, 200)
}, [router])
const handleLogin = async (e?: React.FormEvent) => {
if (e) e.preventDefault()
setError('')
if (!adminToken.trim()) {
setError('Please enter admin token')
return
}
setIsLoading(true)
try {
console.log('Attempting login with token:', adminToken)
// Test API connection first
const testResponse = await fetch('http://127.0.0.1:5000/api/admin/courses', {
headers: { 'Authorization': `Bearer ${adminToken}` }
})
if (testResponse.ok) {
console.log('API accepts token, saving to localStorage')
// Clear any existing token first
localStorage.removeItem('admin_token')
// Save new token
localStorage.setItem('admin_token', adminToken)
// Verify it was saved
const savedToken = localStorage.getItem('admin_token')
console.log('Token saved verification:', savedToken)
if (savedToken === adminToken) {
console.log('✅ Token saved successfully, redirecting...')
// Use window.location for reliable redirect
setTimeout(() => {
window.location.href = '/admin'
}, 100)
} else {
setError('Failed to save authentication. Please try again.')
}
} else {
console.log('API rejected token')
setError('Invalid admin credentials. Please contact administrator.')
setAdminToken('')
}
} catch (err) {
console.error('Login error:', err)
setError('Connection failed. Make sure backend is running.')
} finally {
setIsLoading(false)
}
}
if (!isClient) {
return null
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-purple-50 flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="bg-white rounded-lg shadow-xl p-8">
{/* Header */}
<div className="text-center mb-8">
<div className="w-16 h-16 bg-gradient-to-r from-blue-600 to-purple-600 rounded-full flex items-center justify-center mx-auto mb-4">
<span className="text-white font-bold text-xl">OL</span>
</div>
<h1 className="text-2xl font-bold text-gray-900 mb-2">
OpenLearnX Admin
</h1>
<p className="text-gray-600 text-sm">
Enter your admin credentials to manage courses
</p>
</div>
{/* Login Form */}
<form onSubmit={handleLogin} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Admin Token
</label>
<input
type="password"
placeholder="Enter admin token"
value={adminToken}
onChange={(e) => setAdminToken(e.target.value)}
disabled={isLoading}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50"
autoComplete="off"
/>
</div>
{/* Error Message */}
{error && (
<div className="bg-red-50 border border-red-200 text-red-600 px-3 py-2 rounded text-sm">
{error}
</div>
)}
{/* Login Button */}
<button
type="submit"
disabled={isLoading || !adminToken.trim()}
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-medium py-2 px-4 rounded-md disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isLoading ? (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent mr-2"></div>
Authenticating...
</div>
) : (
'🔐 Login to Admin Panel'
)}
</button>
</form>
{/* Security Notice */}
<div className="mt-6 pt-4 border-t border-gray-100">
<div className="text-center">
<p className="text-xs text-gray-500">
🔒 Secure access only - Contact administrator for credentials
</p>
</div>
</div>
</div>
{/* Footer */}
<div className="text-center mt-4">
<p className="text-sm text-gray-500">
Welcome back, <span className="font-medium text-gray-700">5t4l1n</span>! 👋
</p>
</div>
</div>
</div>
)
}
+636
View File
@@ -0,0 +1,636 @@
'use client'
import React, { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { Plus, Edit, Trash2, Eye, RefreshCw } from 'lucide-react'
interface Course {
id: string
title: string
subject: string
description: string
difficulty: string
mentor: string
video_url: string
students: number
status: 'published' | 'draft'
created_at: string
}
export default function AdminDashboard() {
const [isClient, setIsClient] = useState(false)
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [authChecked, setAuthChecked] = useState(false)
const [courses, setCourses] = useState<Course[]>([])
const [loading, setLoading] = useState(true)
const [showAddForm, setShowAddForm] = useState(false)
const [editingCourse, setEditingCourse] = useState<Course | null>(null)
const [stats, setStats] = useState({
total_courses: 0,
total_lessons: 0,
active_students: 0,
completion_rate: 0
})
const router = useRouter()
// Enhanced authentication with API verification to prevent redirect loops
useEffect(() => {
setIsClient(true)
const checkAuth = async () => {
try {
// Add delay to prevent race conditions
await new Promise(resolve => setTimeout(resolve, 500))
const token = localStorage.getItem('admin_token')
console.log('Dashboard - checking token:', token)
if (!token) {
console.log('Dashboard - no token found')
router.push('/admin/login')
return
}
if (token === 'admin-secret-key') {
console.log('Dashboard - token format valid, verifying with API...')
try {
const response = await fetch('http://127.0.0.1:5000/api/admin/courses', {
headers: { 'Authorization': `Bearer ${token}` }
})
if (response.ok) {
console.log('✅ Dashboard - API confirms token valid')
setIsAuthenticated(true)
fetchData()
} else {
console.log('❌ Dashboard - API rejects token')
localStorage.removeItem('admin_token')
router.push('/admin/login')
}
} catch (apiError) {
console.error('Dashboard - API check failed:', apiError)
// Don't redirect on API error, might be temporary network issue
setIsAuthenticated(true)
fetchData()
}
} else {
console.log('Dashboard - invalid token format')
localStorage.removeItem('admin_token')
router.push('/admin/login')
}
} catch (error) {
console.error('Dashboard - auth check error:', error)
router.push('/admin/login')
} finally {
setAuthChecked(true)
}
}
checkAuth()
}, [router])
const fetchData = async () => {
await Promise.all([fetchCourses(), fetchStats()])
}
const fetchCourses = async () => {
try {
console.log('Fetching courses...')
const response = await fetch('http://127.0.0.1:5000/api/admin/courses', {
headers: {
'Authorization': 'Bearer admin-secret-key',
'Content-Type': 'application/json'
}
})
console.log('Response status:', response.status)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
console.log('Received data:', data)
if (Array.isArray(data)) {
setCourses(data)
console.log('✅ Courses set successfully:', data.length, 'courses')
} else {
console.error('❌ API returned non-array data:', data)
setCourses([])
}
} catch (error) {
console.error('❌ Error fetching courses:', error)
setCourses([])
} finally {
setLoading(false)
}
}
const fetchStats = async () => {
try {
const response = await fetch('http://127.0.0.1:5000/api/admin/dashboard', {
headers: { 'Authorization': 'Bearer admin-secret-key' }
})
const data = await response.json()
setStats(data)
} catch (error) {
console.error('Error fetching stats:', error)
}
}
const handleCreateCourse = async (formData: any) => {
try {
const response = await fetch('http://127.0.0.1:5000/api/admin/courses', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer admin-secret-key'
},
body: JSON.stringify(formData)
})
if (response.ok) {
await fetchData()
setShowAddForm(false)
alert('Course created successfully!')
} else {
const error = await response.json()
alert(`Error: ${error.error}`)
}
} catch (error) {
console.error('Error creating course:', error)
alert('Failed to create course')
}
}
const handleUpdateCourse = async (courseId: string, formData: any) => {
try {
const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer admin-secret-key'
},
body: JSON.stringify(formData)
})
if (response.ok) {
await fetchData()
setEditingCourse(null)
alert('Course updated successfully!')
} else {
const error = await response.json()
alert(`Error: ${error.error}`)
}
} catch (error) {
console.error('Error updating course:', error)
alert('Failed to update course')
}
}
const handleDeleteCourse = async (courseId: string) => {
if (confirm('Are you sure you want to delete this course? This action cannot be undone.')) {
try {
const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}`, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer admin-secret-key' }
})
if (response.ok) {
await fetchData()
alert('Course deleted successfully!')
} else {
const error = await response.json()
alert(`Error: ${error.error}`)
}
} catch (error) {
console.error('Error deleting course:', error)
alert('Failed to delete course')
}
}
}
const initializeDefaultCourses = async () => {
try {
const response = await fetch('http://127.0.0.1:5000/api/admin/initialize', {
method: 'POST',
headers: { 'Authorization': 'Bearer admin-secret-key' }
})
if (response.ok) {
await fetchData()
alert('Default courses initialized!')
}
} catch (error) {
console.error('Error initializing courses:', error)
}
}
const handleLogout = () => {
localStorage.removeItem('admin_token')
router.push('/')
}
// Show loading until auth is checked
if (!isClient || !authChecked) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<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">Checking authentication...</p>
</div>
</div>
)
}
// Show redirect message if not authenticated
if (!isAuthenticated) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<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">Redirecting to login...</p>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<div className="bg-white shadow border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center space-x-4">
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">OL</span>
</div>
<h1 className="text-xl font-bold text-gray-900">OpenLearnX Admin Panel</h1>
<span className="bg-green-100 text-green-800 px-2 py-1 rounded-full text-xs font-medium">
DYNAMIC
</span>
</div>
<div className="flex items-center space-x-4">
<button
onClick={fetchData}
className="p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-md"
title="Refresh Data"
>
<RefreshCw className="h-4 w-4" />
</button>
<span className="text-gray-600">Welcome, 5t4l1n! 👋</span>
<button
onClick={handleLogout}
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded text-sm"
>
Logout
</button>
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6 sm:px-0">
{/* Action Buttons */}
<div className="flex justify-between items-center mb-6">
<div>
<h2 className="text-2xl font-bold text-gray-900">Course Management</h2>
<p className="text-gray-600">Add, edit, or remove courses dynamically</p>
</div>
<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
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"
>
<Plus className="h-4 w-4" />
<span>Add New Course</span>
</button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Total Courses</h3>
<p className="text-2xl font-bold text-blue-600">{stats.total_courses}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Active Students</h3>
<p className="text-2xl font-bold text-green-600">{stats.active_students}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Total Lessons</h3>
<p className="text-2xl font-bold text-purple-600">{stats.total_lessons}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Completion Rate</h3>
<p className="text-2xl font-bold text-orange-600">{stats.completion_rate}%</p>
</div>
</div>
{/* Course Table */}
<div className="bg-white rounded-lg shadow overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">All Courses</h3>
</div>
{loading ? (
<div className="p-8 text-center">
<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>
</div>
) : !Array.isArray(courses) || courses.length === 0 ? (
<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>
<button
onClick={initializeDefaultCourses}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded"
>
Initialize Default Courses
</button>
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Course
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Mentor
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Students
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{courses.map((course) => (
<tr key={course.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-gray-900">
{course.title}
</div>
<div className="text-sm text-gray-500">
{course.subject} {course.difficulty}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{course.mentor}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{course.students?.toLocaleString() || 0}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex space-x-2">
<button
onClick={() => window.open(`/courses/${course.id}`, '_blank')}
className="text-blue-600 hover:text-blue-900"
title="View Course"
>
<Eye className="h-4 w-4" />
</button>
<button
onClick={() => setEditingCourse(course)}
className="text-green-600 hover:text-green-900"
title="Edit Course"
>
<Edit className="h-4 w-4" />
</button>
<button
onClick={() => handleDeleteCourse(course.id)}
className="text-red-600 hover:text-red-900"
title="Delete Course"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
{/* Add Course Modal */}
{showAddForm && (
<CourseFormModal
title="Add New Course"
onClose={() => setShowAddForm(false)}
onSubmit={handleCreateCourse}
/>
)}
{/* Edit Course Modal */}
{editingCourse && (
<CourseFormModal
title="Edit Course"
course={editingCourse}
onClose={() => setEditingCourse(null)}
onSubmit={(data) => handleUpdateCourse(editingCourse.id, data)}
/>
)}
</div>
)
}
// Course Form Modal Component
function CourseFormModal({
title,
course,
onClose,
onSubmit
}: {
title: string
course?: Course
onClose: () => void
onSubmit: (data: any) => void
}) {
const [formData, setFormData] = useState({
title: course?.title || '',
subject: course?.subject || 'Programming',
description: course?.description || '',
difficulty: course?.difficulty || 'Beginner',
mentor: course?.mentor || '5t4l1n',
video_url: course?.video_url || ''
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
onSubmit(formData)
}
const getEmbedUrl = (videoUrl: string) => {
if (!videoUrl) return null
let videoId = ''
if (videoUrl.includes('youtu.be/')) {
videoId = videoUrl.split('youtu.be/')[1]?.split('?')[0]
} else if (videoUrl.includes('youtube.com/watch?v=')) {
videoId = videoUrl.split('v=')[1]?.split('&')[0]
} else if (videoUrl.includes('youtube.com/embed/')) {
return videoUrl
}
return videoId ? `https://www.youtube.com/embed/${videoId}?rel=0&modestbranding=1` : null
}
return (
<React.Fragment>
<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="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Course Title *
</label>
<input
type="text"
required
value={formData.title}
onChange={(e) => setFormData({...formData, 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., Advanced React Development"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Subject *
</label>
<select
value={formData.subject}
onChange={(e) => setFormData({...formData, subject: 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"
>
<option value="Programming">Programming</option>
<option value="Cybersecurity">Cybersecurity</option>
<option value="Web Development">Web Development</option>
<option value="Data Science">Data Science</option>
<option value="Mobile Development">Mobile Development</option>
<option value="DevOps">DevOps</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Difficulty *
</label>
<select
value={formData.difficulty}
onChange={(e) => setFormData({...formData, difficulty: 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"
>
<option value="Beginner">Beginner</option>
<option value="Intermediate">Intermediate</option>
<option value="Advanced">Advanced</option>
<option value="Expert">Expert</option>
<option value="Beginner to Advanced">Beginner to Advanced</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Mentor *
</label>
<input
type="text"
required
value={formData.mentor}
onChange={(e) => setFormData({...formData, mentor: 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="Instructor name"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Description *
</label>
<textarea
required
rows={3}
value={formData.description}
onChange={(e) => setFormData({...formData, 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"
placeholder="Course description..."
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
YouTube Video URL
</label>
<input
type="url"
value={formData.video_url}
onChange={(e) => setFormData({...formData, 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-blue-500"
placeholder="https://youtu.be/..."
/>
</div>
{/* Video Preview */}
{formData.video_url && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Video Preview
</label>
<div className="aspect-video rounded-lg overflow-hidden bg-gray-100">
<iframe
src={getEmbedUrl(formData.video_url) || ''}
title="Video Preview"
className="w-full h-full"
allowFullScreen
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
/>
</div>
</div>
)}
<div className="flex justify-end space-x-3 pt-4 border-t">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-gray-700 border border-gray-300 rounded-md hover:bg-gray-50"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
{course ? 'Update Course' : 'Create Course'}
</button>
</div>
</form>
</div>
</div>
</React.Fragment>
)
}
@@ -6,20 +6,20 @@ import { Loader2 } from "lucide-react"
import { useAuth } from "@/context/auth-context" import { useAuth } from "@/context/auth-context"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { toast } from "react-hot-toast" import { toast } from "react-hot-toast"
import { useState, useEffect } from "react" import { useState, useEffect, use } from "react" // ✅ Added 'use' import
import type { Course } from "@/lib/types" import type { Course } from "@/lib/types"
import api from "@/lib/api" // Corrected import: default import import api from "@/lib/api"
interface CourseDetailPageProps { interface CourseDetailPageProps {
params: { params: Promise<{ // ✅ Changed to Promise
courseId: string courseId: string
lessonId: string lessonId: string
} }>
} }
export default function CourseDetailPage({ params }: CourseDetailPageProps) { export default function CourseDetailPage({ params }: CourseDetailPageProps) {
const { courseId, lessonId } = params const { courseId, lessonId } = use(params) // ✅ Unwrap params using React.use()
const { user, firebaseUser, isLoadingAuth } = useAuth() // Allow firebaseUser const { user, firebaseUser, isLoadingAuth } = useAuth()
const router = useRouter() const router = useRouter()
const [course, setCourse] = useState<Course | null>(null) const [course, setCourse] = useState<Course | null>(null)
const [isLoadingCourse, setIsLoadingCourse] = useState(true) const [isLoadingCourse, setIsLoadingCourse] = useState(true)
@@ -27,7 +27,6 @@ export default function CourseDetailPage({ params }: CourseDetailPageProps) {
useEffect(() => { useEffect(() => {
if (!isLoadingAuth && !user && !firebaseUser) { if (!isLoadingAuth && !user && !firebaseUser) {
// Allow either MetaMask or Firebase user
toast.error("Please login to view courses.") toast.error("Please login to view courses.")
router.push("/") router.push("/")
return return
@@ -37,7 +36,6 @@ export default function CourseDetailPage({ params }: CourseDetailPageProps) {
setIsLoadingCourse(true) setIsLoadingCourse(true)
setError(null) setError(null)
try { try {
// --- ORIGINAL API CALL (UNCOMMENT WHEN BACKEND IS READY) ---
const response = await api.get<Course>(`/api/courses/${courseId}`) const response = await api.get<Course>(`/api/courses/${courseId}`)
setCourse(response.data) setCourse(response.data)
} catch (err: any) { } catch (err: any) {
@@ -50,7 +48,6 @@ export default function CourseDetailPage({ params }: CourseDetailPageProps) {
} }
if (user || firebaseUser) { if (user || firebaseUser) {
// Only fetch if either user type is logged in
fetchCourse() fetchCourse()
} }
}, [user, firebaseUser, isLoadingAuth, router, courseId]) }, [user, firebaseUser, isLoadingAuth, router, courseId])
@@ -82,7 +79,11 @@ export default function CourseDetailPage({ params }: CourseDetailPageProps) {
return ( return (
<div className="flex flex-col md:flex-row min-h-[calc(100vh-64px)]"> <div className="flex flex-col md:flex-row min-h-[calc(100vh-64px)]">
<CourseSidebar courseId={course.id} modules={course.modules} activeLessonId={lessonId} /> <CourseSidebar
courseId={course.id}
modules={course.modules}
activeLessonId={lessonId}
/>
<div className="flex-1 p-4 md:p-8 overflow-y-auto"> <div className="flex-1 p-4 md:p-8 overflow-y-auto">
<LessonViewer courseId={course.id} lessonId={lessonId} /> <LessonViewer courseId={course.id} lessonId={lessonId} />
</div> </div>
+20 -13
View File
@@ -6,19 +6,19 @@ import { Loader2 } from "lucide-react"
import { useAuth } from "@/context/auth-context" import { useAuth } from "@/context/auth-context"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { toast } from "react-hot-toast" import { toast } from "react-hot-toast"
import { useState, useEffect } from "react" import { useState, useEffect, use } from "react" // ✅ Added 'use' import
import type { Course } from "@/lib/types" import type { Course } from "@/lib/types"
import api from "@/lib/api" // Corrected import: default import import api from "@/lib/api"
interface CourseOverviewPageProps { interface CourseOverviewPageProps {
params: { params: Promise<{ // ✅ Changed to Promise
courseId: string courseId: string
} }>
} }
export default function CourseOverviewPage({ params }: CourseOverviewPageProps) { export default function CourseOverviewPage({ params }: CourseOverviewPageProps) {
const { courseId } = params const { courseId } = use(params) // ✅ Unwrap params using React.use()
const { user, firebaseUser, isLoadingAuth } = useAuth() // Allow firebaseUser const { user, firebaseUser, isLoadingAuth } = useAuth()
const router = useRouter() const router = useRouter()
const [course, setCourse] = useState<Course | null>(null) const [course, setCourse] = useState<Course | null>(null)
const [isLoadingCourse, setIsLoadingCourse] = useState(true) const [isLoadingCourse, setIsLoadingCourse] = useState(true)
@@ -26,7 +26,6 @@ export default function CourseOverviewPage({ params }: CourseOverviewPageProps)
useEffect(() => { useEffect(() => {
if (!isLoadingAuth && !user && !firebaseUser) { if (!isLoadingAuth && !user && !firebaseUser) {
// Allow either MetaMask or Firebase user
toast.error("Please login to view courses.") toast.error("Please login to view courses.")
router.push("/") router.push("/")
return return
@@ -36,7 +35,6 @@ export default function CourseOverviewPage({ params }: CourseOverviewPageProps)
setIsLoadingCourse(true) setIsLoadingCourse(true)
setError(null) setError(null)
try { try {
// --- ORIGINAL API CALL (UNCOMMENT WHEN BACKEND IS READY) ---
const response = await api.get<Course>(`/api/courses/${courseId}`) const response = await api.get<Course>(`/api/courses/${courseId}`)
setCourse(response.data) setCourse(response.data)
} catch (err: any) { } catch (err: any) {
@@ -49,7 +47,6 @@ export default function CourseOverviewPage({ params }: CourseOverviewPageProps)
} }
if (user || firebaseUser) { if (user || firebaseUser) {
// Only fetch if either user type is logged in
fetchCourse() fetchCourse()
} }
}, [user, firebaseUser, isLoadingAuth, router, courseId]) }, [user, firebaseUser, isLoadingAuth, router, courseId])
@@ -88,15 +85,25 @@ export default function CourseOverviewPage({ params }: CourseOverviewPageProps)
return ( return (
<div className="flex flex-col md:flex-row min-h-[calc(100vh-64px)]"> <div className="flex flex-col md:flex-row min-h-[calc(100vh-64px)]">
<CourseSidebar courseId={course.id} modules={course.modules} activeLessonId="" /> <CourseSidebar
courseId={course.id}
modules={course.modules}
activeLessonId=""
/>
<div className="flex-1 p-4 md:p-8 overflow-y-auto"> <div className="flex-1 p-4 md:p-8 overflow-y-auto">
<Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100"> <Card className="bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100">
<CardHeader> <CardHeader>
<CardTitle className="text-2xl font-bold text-primary-purple">{course.title} Overview</CardTitle> <CardTitle className="text-2xl font-bold text-primary-purple">
{course.title} Overview
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<p className="text-lg text-gray-700 dark:text-gray-200">{course.description}</p> <p className="text-lg text-gray-700 dark:text-gray-200">
<p className="text-gray-600 dark:text-gray-300">Select a lesson from the sidebar to begin.</p> {course.description}
</p>
<p className="text-gray-600 dark:text-gray-300">
Select a lesson from the sidebar to begin.
</p>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
+231 -31
View File
@@ -1,7 +1,6 @@
"use client" "use client"
import Link from "next/link" import Link from "next/link"
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { useAuth } from "@/context/auth-context" import { useAuth } from "@/context/auth-context"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
@@ -9,26 +8,37 @@ import { toast } from "react-hot-toast"
import type { Lesson } from "@/lib/types" import type { Lesson } from "@/lib/types"
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Loader2 } from "lucide-react" import { Loader2, Play, BookOpen, Clock } from "lucide-react"
import ReactMarkdown from "react-markdown" import ReactMarkdown from "react-markdown"
import { api } from "@/lib/api"
interface LessonViewerProps { interface LessonViewerProps {
courseId: string courseId: string
lessonId: string lessonId: string
} }
interface LessonData {
id: string
title: string
type: string
video_url: string
embed_url: string
duration: string
description: string
content: string
course_id: string
completed?: boolean
}
export function LessonViewer({ courseId, lessonId }: LessonViewerProps) { export function LessonViewer({ courseId, lessonId }: LessonViewerProps) {
const { user, firebaseUser, isLoadingAuth, authMethod, token } = useAuth() // Check token for access const { user, firebaseUser, isLoadingAuth, authMethod, token } = useAuth()
const router = useRouter() const router = useRouter()
const [lesson, setLesson] = useState<Lesson | null>(null) const [lesson, setLesson] = useState<LessonData | null>(null)
const [isLoadingLesson, setIsLoadingLesson] = useState(true) const [isLoadingLesson, setIsLoadingLesson] = useState(true)
const [isMarkingCompleted, setIsMarkingCompleted] = useState(false) const [isMarkingCompleted, setIsMarkingCompleted] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
if (!isLoadingAuth && !user && !firebaseUser) { if (!isLoadingAuth && !user && !firebaseUser) {
// Allow either MetaMask or Firebase user
toast.error("Please login to view lessons.") toast.error("Please login to view lessons.")
router.push("/") router.push("/")
return return
@@ -38,19 +48,35 @@ export function LessonViewer({ courseId, lessonId }: LessonViewerProps) {
setIsLoadingLesson(true) setIsLoadingLesson(true)
setError(null) setError(null)
try { try {
const response = await api.get<Lesson>(`/api/courses/${courseId}/lessons/${lessonId}`) console.log(`Fetching lesson: ${courseId}/${lessonId}`) // Debug log
setLesson(response.data)
const response = await fetch(`http://127.0.0.1:5000/api/courses/${courseId}/lessons/${lessonId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
// Add auth header if token exists
...(token && { 'Authorization': `Bearer ${token}` })
}
})
if (!response.ok) {
throw new Error(`Failed to fetch lesson: ${response.status}`)
}
const lessonData = await response.json()
console.log('Lesson data received:', lessonData) // Debug log
setLesson(lessonData)
} catch (err: any) { } catch (err: any) {
console.error("Failed to fetch lesson:", err) console.error("Failed to fetch lesson:", err)
setError(err.response?.data?.message || "Failed to load lesson.") setError(err.message || "Failed to load lesson.")
toast.error(err.response?.data?.message || "Failed to load lesson.") toast.error(err.message || "Failed to load lesson.")
} finally { } finally {
setIsLoadingLesson(false) setIsLoadingLesson(false)
} }
} }
if (user || firebaseUser) { if (user || firebaseUser) {
// Only fetch if either user type is logged in
fetchLesson() fetchLesson()
} }
}, [user, firebaseUser, isLoadingAuth, router, courseId, lessonId, token]) }, [user, firebaseUser, isLoadingAuth, router, courseId, lessonId, token])
@@ -58,24 +84,108 @@ export function LessonViewer({ courseId, lessonId }: LessonViewerProps) {
const markLessonCompleted = async () => { const markLessonCompleted = async () => {
if (!lesson || lesson.completed || !token) { if (!lesson || lesson.completed || !token) {
toast.error("Please connect your MetaMask wallet to mark lessons as completed.") toast.error("Please connect your MetaMask wallet to mark lessons as completed.")
return // Ensure token exists for this action return
} }
setIsMarkingCompleted(true) setIsMarkingCompleted(true)
try { try {
await api.post(`/api/courses/${courseId}/lessons/${lessonId}/complete`) const response = await fetch(`http://127.0.0.1:5000/api/courses/${courseId}/lessons/${lessonId}/complete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
})
if (response.ok) {
setLesson((prev) => (prev ? { ...prev, completed: true } : null)) setLesson((prev) => (prev ? { ...prev, completed: true } : null))
toast.success("Lesson marked as completed!") toast.success("Lesson marked as completed!")
} else {
throw new Error('Failed to mark lesson as completed')
}
} catch (err: any) { } catch (err: any) {
console.error("Failed to mark lesson completed:", err) console.error("Failed to mark lesson completed:", err)
toast.error(err.response?.data?.message || "Failed to mark lesson completed.") toast.error("Failed to mark lesson completed.")
} finally { } finally {
setIsMarkingCompleted(false) setIsMarkingCompleted(false)
} }
} }
// Fixed: Function to render markdown-like content with proper string handling
const renderContent = (content: string) => {
const lines = content.split('\n')
const result = []
let inCodeBlock = false
let codeBlockContent: string[] = []
for (let index = 0; index < lines.length; index++) {
const line = lines[index]
// Handle code blocks properly
if (line.startsWith('```')) {
if (!inCodeBlock) {
// Opening code block
inCodeBlock = true
codeBlockContent = []
} else {
// Closing code block - render accumulated content
inCodeBlock = false
result.push(
<div key={`code-${index}`} className="bg-gray-900 text-green-400 p-4 rounded-lg font-mono text-sm mt-4 mb-4 overflow-x-auto">
<div className="flex items-center mb-3">
<span className="text-white font-semibold">💻 Code Example:</span>
</div>
<pre className="text-sm">
<code>{codeBlockContent.join('\n')}</code>
</pre>
</div>
)
codeBlockContent = []
}
continue
}
// If we're inside a code block, accumulate content
if (inCodeBlock) {
codeBlockContent.push(line)
continue
}
// Handle other markdown elements
if (line.startsWith('# ')) {
result.push(
<h1 key={index} className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">
{line.substring(2)}
</h1>
)
} else if (line.startsWith('## ')) {
result.push(
<h2 key={index} className="text-xl font-semibold mb-3 text-gray-800 dark:text-gray-200 mt-6">
{line.substring(3)}
</h2>
)
} else if (line.startsWith('- ')) {
result.push(
<li key={index} className="ml-4 text-gray-700 dark:text-gray-300 list-disc">
{line.substring(2)}
</li>
)
} else if (line.trim() === '') {
result.push(<br key={index} />)
} else if (line.trim() !== '') {
result.push(
<p key={index} className="text-gray-700 dark:text-gray-300 mb-2">
{line}
</p>
)
}
}
return result
}
if (isLoadingAuth || isLoadingLesson) { if (isLoadingAuth || isLoadingLesson) {
return ( return (
<div className="flex justify-center items-center flex-1"> <div className="flex justify-center items-center flex-1 min-h-[400px]">
<Loader2 className="h-8 w-8 animate-spin text-primary-purple" /> <Loader2 className="h-8 w-8 animate-spin text-primary-purple" />
<span className="ml-2 text-lg">Loading lesson...</span> <span className="ml-2 text-lg">Loading lesson...</span>
</div> </div>
@@ -85,7 +195,12 @@ export function LessonViewer({ courseId, lessonId }: LessonViewerProps) {
if (error) { if (error) {
return ( return (
<div className="flex justify-center items-center flex-1 text-red-500"> <div className="flex justify-center items-center flex-1 text-red-500">
<p>{error}</p> <div className="text-center">
<p className="text-xl mb-4">{error}</p>
<Button onClick={() => window.location.reload()} variant="outline">
Retry Loading
</Button>
</div>
</div> </div>
) )
} }
@@ -99,31 +214,92 @@ export function LessonViewer({ courseId, lessonId }: LessonViewerProps) {
} }
return ( return (
<Card className="flex-1 bg-white shadow-md rounded-lg p-6 dark:bg-gray-800 dark:text-gray-100"> <div className="flex-1 space-y-6">
{/* Lesson Header Card */}
<Card className="bg-gradient-to-r from-blue-600 to-purple-600 text-white">
<CardHeader> <CardHeader>
<CardTitle className="text-2xl font-bold text-primary-purple">{lesson.title}</CardTitle> <CardTitle className="text-2xl font-bold mb-2">{lesson.title}</CardTitle>
{lesson.description && (
<p className="text-blue-100 mb-4">{lesson.description}</p>
)}
<div className="flex items-center space-x-4 flex-wrap">
<span className="bg-white/20 px-3 py-1 rounded-full text-sm flex items-center">
{lesson.type === 'video' ? <Play className="w-4 h-4 mr-1" /> : <BookOpen className="w-4 h-4 mr-1" />}
{lesson.type === 'video' ? 'Video Lesson' : lesson.type === 'code' ? 'Coding Exercise' : 'Reading'}
</span>
{lesson.duration && (
<span className="bg-white/20 px-3 py-1 rounded-full text-sm flex items-center">
<Clock className="w-4 h-4 mr-1" />
{lesson.duration}
</span>
)}
{lesson.completed && (
<span className="bg-green-500 px-3 py-1 rounded-full text-sm">
Completed
</span>
)}
</div>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> </Card>
{/* Authentication Warning */}
{authMethod === "firebase" && !token && ( {authMethod === "firebase" && !token && (
<div className="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 rounded-md dark:bg-yellow-900 dark:border-yellow-600 dark:text-yellow-200"> <Card className="bg-yellow-50 border-l-4 border-yellow-500 dark:bg-yellow-900">
<p className="font-bold">Limited Access</p> <CardContent className="pt-6">
<p> <p className="font-bold text-yellow-700 dark:text-yellow-200">Limited Access</p>
<p className="text-yellow-700 dark:text-yellow-200">
You are logged in with email. Full functionality, including progress tracking and data persistence, You are logged in with email. Full functionality, including progress tracking and data persistence,
requires connecting your MetaMask wallet. requires connecting your MetaMask wallet.
</p> </p>
</div> </CardContent>
</Card>
)} )}
{lesson.type === "video" && (
<div className="aspect-video w-full bg-gray-200 dark:bg-gray-700 rounded-md flex items-center justify-center text-gray-500"> {/* Video Player */}
<p>Video Player Placeholder: {lesson.content}</p> {lesson.type === "video" && lesson.embed_url && (
{/* In a real app, you'd embed a video player here */} <Card>
<CardContent className="p-6">
<h3 className="text-xl font-bold mb-4 flex items-center">
<Play className="w-5 h-5 mr-2" />
Video Tutorial
</h3>
<div className="aspect-video rounded-lg overflow-hidden shadow-lg bg-gray-100">
<iframe
src={lesson.embed_url}
title={lesson.title}
className="w-full h-full"
allowFullScreen
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
style={{ border: 'none' }}
/>
</div> </div>
</CardContent>
</Card>
)} )}
{/* Lesson Content */}
<Card className="bg-white shadow-md rounded-lg dark:bg-gray-800">
<CardHeader>
<CardTitle className="text-xl font-bold text-primary-purple flex items-center">
<BookOpen className="w-5 h-5 mr-2" />
Lesson Content
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{lesson.type === "text" && ( {lesson.type === "text" && (
<div className="prose dark:prose-invert max-w-none"> <div className="prose dark:prose-invert max-w-none">
<ReactMarkdown>{lesson.content}</ReactMarkdown> <ReactMarkdown>{lesson.content}</ReactMarkdown>
</div> </div>
)} )}
{lesson.type === "video" && lesson.content && (
<div className="prose dark:prose-invert max-w-none">
<div className="space-y-2">
{renderContent(lesson.content)}
</div>
</div>
)}
{lesson.type === "code" && ( {lesson.type === "code" && (
<div className="bg-gray-900 text-white p-4 rounded-md font-mono text-sm overflow-x-auto"> <div className="bg-gray-900 text-white p-4 rounded-md font-mono text-sm overflow-x-auto">
<pre> <pre>
@@ -131,6 +307,7 @@ export function LessonViewer({ courseId, lessonId }: LessonViewerProps) {
</pre> </pre>
</div> </div>
)} )}
{lesson.type === "quiz" && ( {lesson.type === "quiz" && (
<div className="bg-blue-50 dark:bg-blue-900 p-4 rounded-md text-primary-blue dark:text-blue-200"> <div className="bg-blue-50 dark:bg-blue-900 p-4 rounded-md text-primary-blue dark:text-blue-200">
<p className="font-semibold">Quiz Link:</p> <p className="font-semibold">Quiz Link:</p>
@@ -140,11 +317,16 @@ export function LessonViewer({ courseId, lessonId }: LessonViewerProps) {
</div> </div>
)} )}
</CardContent> </CardContent>
<CardFooter className="flex justify-end">
<CardFooter className="flex justify-between items-center">
<div className="text-sm text-gray-500">
Course: {lesson.course_id} | Lesson: {lesson.id}
</div>
<div className="flex space-x-2">
{!lesson.completed && ( {!lesson.completed && (
<Button <Button
onClick={markLessonCompleted} onClick={markLessonCompleted}
disabled={isMarkingCompleted || !token} // Disable if no token disabled={isMarkingCompleted || !token}
className="bg-primary-purple hover:bg-primary-purple/90 text-white" className="bg-primary-purple hover:bg-primary-purple/90 text-white"
> >
{isMarkingCompleted && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} {isMarkingCompleted && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
@@ -152,11 +334,29 @@ export function LessonViewer({ courseId, lessonId }: LessonViewerProps) {
</Button> </Button>
)} )}
{lesson.completed && ( {lesson.completed && (
<Button disabled variant="outline" className="text-success border-success bg-transparent"> <Button disabled variant="outline" className="text-green-600 border-green-600 bg-transparent">
Completed Completed
</Button> </Button>
)} )}
</div>
</CardFooter> </CardFooter>
</Card> </Card>
{/* Practice Section for Learning */}
<Card className="border-l-4 border-l-green-500">
<CardContent className="p-6">
<h3 className="text-lg font-bold mb-3 text-green-700">🚀 Practice Exercise</h3>
<p className="text-gray-700 dark:text-gray-300">
Try modifying the code examples! Experiment with:
</p>
<ul className="list-disc list-inside mt-2 text-gray-700 dark:text-gray-300 space-y-1">
<li>Changing colors and shapes</li>
<li>Adding your own creative elements</li>
<li>Exploring different programming concepts</li>
<li>Building upon what you&apos;ve learned</li>
</ul>
</CardContent>
</Card>
</div>
) )
} }
+15
View File
@@ -192,3 +192,18 @@ pnpm run dev
``` ```
## run mangodb in local for running
```
# Install MongoDB on Arch Linux
yay -S mongodb-bin
# Start the service
sudo systemctl start mongodb
sudo systemctl enable mongodb
# Verify it's running
sudo systemctl status mongodb
# Test connection
mongosh
```