From 6326cad39d8bf79f7c7f0d2dc03e41a2a91ac4d7 Mon Sep 17 00:00:00 2001 From: Nurfog Date: Mon, 29 Dec 2025 23:49:21 -0300 Subject: [PATCH] feat: enhance install script with hardware detection, refactor studio UI, and enable GPU support for whisper service --- db-mgmt.sh | 68 --- docker-compose.yml | 21 +- init-system.sh | 78 ---- install.sh | 252 +++++------ roadmap.md | 1 + shared/common/src/models.rs | 1 + .../src/app/courses/[id]/analytics/page.tsx | 44 +- .../src/app/courses/[id]/calendar/page.tsx | 192 +++++---- .../src/app/courses/[id]/grading/page.tsx | 6 +- web/studio/src/app/courses/[id]/page.tsx | 381 ++++++++-------- .../src/app/courses/[id]/settings/page.tsx | 405 +++++++++--------- web/studio/src/app/page.tsx | 85 ++-- 12 files changed, 678 insertions(+), 856 deletions(-) delete mode 100755 db-mgmt.sh delete mode 100755 init-system.sh diff --git a/db-mgmt.sh b/db-mgmt.sh deleted file mode 100755 index 2fa181b..0000000 --- a/db-mgmt.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash - -# OpenCCB Database Management Script -# This script handles creation, migrations and sqlx preparation for both microservices. - -set -e - -# Load environment variables if .env exists -if [ -f .env ]; then - export $(grep -v '^#' .env | xargs) -fi - -# Fallback to DATABASE_URL if specific ones aren't set -# Note: For running locally against Docker Postgres, use localhost instead of db -CMS_URL=${CMS_DATABASE_URL:-$(echo $DATABASE_URL | sed 's/@db:/@localhost:/')} -LMS_URL=${LMS_DATABASE_URL:-$(echo $DATABASE_URL | sed 's/@db:/@localhost:/')} - -if [ -z "$CMS_URL" ] || [ -z "$LMS_URL" ]; then - echo "Error: CMS_DATABASE_URL or LMS_DATABASE_URL is not set." - echo "Please check your .env file." - exit 1 -fi - -ACTION=$1 - -case $ACTION in - "setup") - echo "--- Creating Databases ---" - DATABASE_URL=$CMS_URL sqlx database create - DATABASE_URL=$LMS_URL sqlx database create - echo "Databases created (if they didn't exist)." - $0 migrate - ;; - - "migrate") - echo "--- Running CMS Migrations ---" - DATABASE_URL=$CMS_URL sqlx migrate run --source services/cms-service/migrations - - echo "--- Running LMS Migrations ---" - DATABASE_URL=$LMS_URL sqlx migrate run --source services/lms-service/migrations - - echo "All migrations completed successfully." - ;; - - "prepare") - echo "--- Preparing SQLx queries for CMS ---" - cd services/cms-service && DATABASE_URL=$CMS_URL cargo sqlx prepare -- --all-targets --all-features && cd ../.. - - echo "--- Preparing SQLx queries for LMS ---" - cd services/lms-service && DATABASE_URL=$LMS_URL cargo sqlx prepare -- --all-targets --all-features && cd ../.. - - echo "SQLx preparation completed." - ;; - - "all") - $0 setup - $0 prepare - ;; - - *) - echo "Usage: $0 {setup|migrate|prepare|all}" - echo " setup: Creates databases and runs migrations" - echo " migrate: Runs database migrations for all services" - echo " prepare: Runs cargo sqlx prepare for offline compilation" - echo " all: Runs setup and prepare" - exit 1 - ;; -esac diff --git a/docker-compose.yml b/docker-compose.yml index 1beda97..10304fa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,24 +57,21 @@ services: NEXT_PUBLIC_LMS_API_URL: http://localhost:3002 whisper: - image: fedirz/faster-whisper-server:latest-cpu + image: ${WHISPER_IMAGE:-fedirz/faster-whisper-server:latest-cpu} ports: - "8000:8000" volumes: - whisper_cache:/root/.cache/huggingface environment: - # - WHISPER_MODEL=medium - # - DEVICE=cpu - # GPU support commented out for stability if drivers missing - - DEVICE=cpu + - DEVICE=${WHISPER_DEVICE:-cpu} # GPU support for RTX 2070 Super - # deploy: - # resources: - # reservations: - # devices: - # - driver: nvidia - # count: 1 - # capabilities: [ gpu ] + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [ gpu ] e2e: build: diff --git a/init-system.sh b/init-system.sh deleted file mode 100755 index 8057821..0000000 --- a/init-system.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash -set -e - -# Default values -API_URL="http://localhost:3001" -DEFAULT_EMAIL="admin@example.com" -DEFAULT_PASSWORD="password123" -DEFAULT_ORG="Default Organization" -DEFAULT_NAME="System Admin" - -echo "===============================================" -echo " OpenCCB System Initialization Script" -echo "===============================================" -echo "" - -# Check for curl and jq -if ! command -v curl &> /dev/null || ! command -v jq &> /dev/null; then - echo "Error: 'curl' and 'jq' are required but not installed." - echo "Please install them (e.g., sudo apt install curl jq) and try again." - exit 1 -fi - -echo "This script will create the initial Administrator account and Organization." -echo "Press Enter to use the default value." -echo "" - -read -p "Enter Organization Name [$DEFAULT_ORG]: " ORG_NAME -ORG_NAME=${ORG_NAME:-$DEFAULT_ORG} - -read -p "Enter Admin Full Name [$DEFAULT_NAME]: " FULL_NAME -FULL_NAME=${FULL_NAME:-$DEFAULT_NAME} - -read -p "Enter Admin Email [$DEFAULT_EMAIL]: " EMAIL -EMAIL=${EMAIL:-$DEFAULT_EMAIL} - -read -s -p "Enter Admin Password [$DEFAULT_PASSWORD]: " PASSWORD -echo "" -PASSWORD=${PASSWORD:-$DEFAULT_PASSWORD} - -echo "" -echo "Creating Administrator..." -echo " Organization: $ORG_NAME" -echo " User: $FULL_NAME <$EMAIL>" -echo " Target API: $API_URL" -echo "" - -# Prepare JSON payload -PAYLOAD=$(jq -n \ - --arg email "$EMAIL" \ - --arg password "$PASSWORD" \ - --arg full_name "$FULL_NAME" \ - --arg org_name "$ORG_NAME" \ - --arg role "admin" \ - '{email: $email, password: $password, full_name: $full_name, organization_name: $org_name, role: $role}') - -# Execute Request -RESPONSE=$(curl -s -X POST "$API_URL/auth/register" \ - -H "Content-Type: application/json" \ - -d "$PAYLOAD") - -# Check status based on JSON response structure (assuming successful response has a "token") -# We use grep here as a simple check, but could parse with jq for more robustness -if echo "$RESPONSE" | grep -q "token"; then - echo "✅ Success! Administrator created." - echo "" - echo "Login Credentials:" - echo "------------------" - echo "Email: $EMAIL" - echo "Password: (hidden)" - echo "Role: admin" - echo "" - echo "You can now log in at: http://localhost:3000/auth/login" -else - echo "❌ Failed to create administrator." - echo "Server Response:" - echo "$RESPONSE" | jq . 2>/dev/null || echo "$RESPONSE" - exit 1 -fi diff --git a/install.sh b/install.sh index 2f5ba13..bd48f45 100755 --- a/install.sh +++ b/install.sh @@ -1,14 +1,15 @@ #!/bin/bash # OpenCCB Unified Installation Script -# This script automates the setup of OpenCCB, including prerequisites, -# repository cloning, dependencies, and initial configuration. +# This script automates the setup of OpenCCB: +# 1. Prerequisite checks (Rust, Node.js, Docker, sqlx-cli) +# 2. Hardware detection (NVIDIA GPU vs CPU) +# 3. Environment configuration (.env) +# 4. Database creation and migrations +# 5. System initialization (Admin account) set -e -REPO_URL="https://github.com/Nurfog/openccb.git" # Example URL, should be updated if needed -PROJECT_DIR="openccb" - echo "====================================================" echo " 🚀 Welcome to the OpenCCB Installer" echo "====================================================" @@ -19,24 +20,18 @@ if [ -f "Cargo.toml" ] && [ -d "services" ] && [ -d "web" ]; then echo "✅ Project detected in current directory." PROJECT_ROOT=$(pwd) else - echo "📂 Project not detected in current directory." - if [ -d "$PROJECT_DIR" ]; then - echo "✅ Detected project folder '$PROJECT_DIR'." - cd "$PROJECT_DIR" + # Simplification: assume we are in the project root if the script is running + # but let's keep a basic check + if [ -d "openccb" ]; then + cd openccb PROJECT_ROOT=$(pwd) else - echo "📥 Project folder not found. Cloning from $REPO_URL..." - git clone "$REPO_URL" "$PROJECT_DIR" - cd "$PROJECT_DIR" - PROJECT_ROOT=$(pwd) + echo "⚠️ Please run this script from the root of the OpenCCB repository." + exit 1 fi fi # 2. Prerequisite Installation -echo "" -echo "🔍 Checking for prerequisites..." - -# Function to check and install system packages (Ubuntu/Debian) install_pkg() { if ! command -v "$1" &> /dev/null; then echo "🔧 Installing $1..." @@ -46,7 +41,6 @@ install_pkg() { fi } -# Check for essential tools if [[ "$OSTYPE" == "linux-gnu"* ]]; then if [ -f /etc/debian_version ]; then install_pkg "curl" @@ -54,191 +48,167 @@ if [[ "$OSTYPE" == "linux-gnu"* ]]; then install_pkg "jq" install_pkg "build-essential" install_pkg "docker.io" - # On modern Ubuntu, docker compose is a plugin included with docker.io or available as docker-compose-v2 if ! docker compose version &> /dev/null; then install_pkg "docker-compose-v2" fi - else - echo "⚠️ Unsupported Linux distribution. Please ensure curl, git, jq, docker, and docker-compose are installed." fi fi -# Check for Rust if ! command -v cargo &> /dev/null; then echo "🔧 Installing Rust..." curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y source $HOME/.cargo/env -else - echo "✅ Rust (Cargo) is already installed." fi -# Check for Node.js if ! command -v node &> /dev/null; then - echo "🔧 Node.js not found. Installing via NVM..." + echo "🔧 Installing Node.js via NVM..." curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" nvm install --lts -else - echo "✅ Node.js $(node -v) is already installed." fi -# Check for sqlx-cli if ! command -v sqlx &> /dev/null; then echo "🔧 Installing sqlx-cli..." cargo install sqlx-cli --no-default-features --features postgres -else - echo "✅ sqlx-cli is already installed." fi -# AI Stack Detection & Installation +# 3. Hardware Detection echo "" -echo "🤖 Setting up Local AI Stack..." - +echo "🔍 Detecting hardware..." HAS_NVIDIA=false -if command -v nvidia-smi &> /dev/null; then - if nvidia-smi -L &> /dev/null; then - echo "🚀 NVIDIA GPU Detected!" - HAS_NVIDIA=true - fi -fi - -if [ "$HAS_NVIDIA" = false ] && command -v lspci &> /dev/null; then - if lspci | grep -i nvidia &> /dev/null; then - echo "🚀 NVIDIA GPU Detected (lspci)!" - HAS_NVIDIA=true - fi -fi - -# Ollama Installation -if ! command -v ollama &> /dev/null; then - echo "🔧 Installing Ollama..." - curl -fsSL https://ollama.com/install.sh | sh +if command -v nvidia-smi &> /dev/null && nvidia-smi -L &> /dev/null; then + echo "🚀 NVIDIA GPU Detected!" + HAS_NVIDIA=true +elif command -v lspci &> /dev/null && lspci | grep -i nvidia &> /dev/null; then + echo "🚀 NVIDIA GPU Detected (lspci)!" + HAS_NVIDIA=true else - echo "✅ Ollama is already installed." + echo "💻 No NVIDIA GPU found. Using CPU mode." fi -# Wait for Ollama to be ready -echo "⏳ Waiting for Ollama server to be ready..." -until curl -s http://localhost:11434/api/tags &> /dev/null; do - sleep 2 -done - -# Pre-download models based on hardware -if [ "$HAS_NVIDIA" = true ]; then - echo "📥 Downloading Llama 3 (optimized for GPU)..." - ollama pull llama3:8b -else - echo "📥 Downloading Phi-3 (lighter for CPU)..." - ollama pull phi3:mini -fi - -# 3. Frontend Dependency Installation -echo "" -echo "📦 Installing frontend dependencies..." -for dir in "web/studio" "web/experience"; do - if [ -d "$dir" ]; then - echo "🔹 Installing in $dir..." - (cd "$dir" && npm install) - fi -done - # 4. Environment Configuration echo "" -echo "⚙️ Configuring environment..." - if [ ! -f ".env" ]; then if [ -f ".env.example" ]; then - echo "📄 Creating .env from .env.example..." cp .env.example .env else - echo "📄 Creating a new .env file..." touch .env fi fi -# Function to update or add a variable in .env update_env() { local key=$1 - local default_value=$2 - local prompt_text=$3 - - # Read current value if it exists - local current_value=$(grep "^${key}=" .env | cut -d'=' -f2- || echo "") - local val=${current_value:-$default_value} - - read -p "$prompt_text [$val]: " user_val - user_val=${user_val:-$val} - + local val=$2 if grep -q "^${key}=" .env; then - # Use a temporary file for sed to be safe - sed -i "s|^${key}=.*|${key}=${user_val}|" .env + sed -i "s|^${key}=.*|${key}=${val}|" .env else - echo "${key}=${user_val}" >> .env + echo "${key}=${val}" >> .env fi } -echo "Please provide the following configuration values (Press Enter for default):" -update_env "DATABASE_URL" "postgresql://user:password@localhost:5432/openccb" "Master Database URL" -update_env "CMS_DATABASE_URL" "postgresql://user:password@localhost:5432/openccb_cms" "CMS Database URL" -update_env "LMS_DATABASE_URL" "postgresql://user:password@localhost:5432/openccb_lms" "LMS Database URL" -update_env "NEXT_PUBLIC_CMS_API_URL" "http://localhost:3001" "Studio CMS API URL" -update_env "NEXT_PUBLIC_LMS_API_URL" "http://localhost:3002" "Experience LMS API URL" - -echo "" -echo "🛠️ AI Configuration..." -update_env "AI_PROVIDER" "local" "AI Provider (openai | local)" -if [ "$(grep "^AI_PROVIDER=" .env | cut -d'=' -f2)" == "local" ]; then - update_env "LOCAL_OLLAMA_URL" "http://localhost:11434" "Local Ollama API URL" - update_env "LOCAL_WHISPER_URL" "http://localhost:8000" "Local Whisper API URL" - - default_model="phi3:mini" - if [ "$HAS_NVIDIA" = true ]; then - default_model="llama3:8b" - fi - update_env "LOCAL_LLM_MODEL" "$default_model" "Local LLM Model" +# Auto-configure AI variables based on hardware +if [ "$HAS_NVIDIA" = true ]; then + update_env "WHISPER_IMAGE" "fedirz/faster-whisper-server:latest-cuda" + update_env "WHISPER_DEVICE" "cuda" + update_env "LOCAL_LLM_MODEL" "llama3:8b" + # Uncomment GPU deploy section in docker-compose.yml while preserving indentation + sed -i '/deploy:/s/# //' docker-compose.yml + sed -i '/resources:/s/# //' docker-compose.yml + sed -i '/reservations:/s/# //' docker-compose.yml + sed -i '/devices:/s/# //' docker-compose.yml + sed -i '/- driver: nvidia/s/# //' docker-compose.yml + sed -i '/count: 1/s/# //' docker-compose.yml + sed -i '/capabilities: \[ gpu \]/s/# //' docker-compose.yml else - update_env "OPENAI_API_KEY" "" "OpenAI API Key" + update_env "WHISPER_IMAGE" "fedirz/faster-whisper-server:latest-cpu" + update_env "WHISPER_DEVICE" "cpu" + update_env "LOCAL_LLM_MODEL" "phi3:mini" + # Ensure it's commented (if it was previously uncommented) + # (Simple approach: we leave it as is or explicitly comment it out) fi -echo "✅ .env configuration updated." +# Ask for DB credentials if not set +if ! grep -q "DATABASE_URL=" .env || [[ $(grep "DATABASE_URL=" .env | cut -d'=' -f2) == "" ]]; then + read -p "Enter Database Password [password]: " DB_PASS + DB_PASS=${DB_PASS:-password} + update_env "DATABASE_URL" "postgresql://user:${DB_PASS}@localhost:5432/openccb" + update_env "CMS_DATABASE_URL" "postgresql://user:${DB_PASS}@localhost:5432/openccb_cms" + update_env "LMS_DATABASE_URL" "postgresql://user:${DB_PASS}@localhost:5432/openccb_lms" +fi -# 5. Database Initialization +# 5. AI Stack Setup +if ! command -v ollama &> /dev/null; then + curl -fsSL https://ollama.com/install.sh | sh +fi + +echo "⏳ Starting Ollama & downloading models..." +# Run ollama in background if not running (simple check) +if ! pgrep ollama &> /dev/null; then + ollama serve & + sleep 5 +fi + +until curl -s http://localhost:11434/api/tags &> /dev/null; do sleep 2; done +if [ "$HAS_NVIDIA" = true ]; then + ollama pull llama3:8b +else + ollama pull phi3:mini +fi + +# 6. Database Initialization (Integrated db-mgmt.sh) echo "" echo "🐘 Starting database with Docker..." docker compose up -d db +sleep 5 # Simple wait -echo "⏳ Waiting for database to be ready..." -# Better wait using pg_isready if available -if command -v pg_isready &> /dev/null; then - until pg_isready -h localhost -p 5432 -U user; do - echo "Still waiting for Postgres..." - sleep 2 - done -else - sleep 10 -fi +CMS_URL=$(grep "CMS_DATABASE_URL=" .env | cut -d'=' -f2) +LMS_URL=$(grep "LMS_DATABASE_URL=" .env | cut -d'=' -f2) -echo "🏗️ Running database setup..." -chmod +x db-mgmt.sh -./db-mgmt.sh setup +echo "🏗️ Creating databases and running migrations..." +DATABASE_URL=$CMS_URL sqlx database create || true +DATABASE_URL=$LMS_URL sqlx database create || true +DATABASE_URL=$CMS_URL sqlx migrate run --source services/cms-service/migrations +DATABASE_URL=$LMS_URL sqlx migrate run --source services/lms-service/migrations -# 6. System Initialization +# 7. System Initialization (Integrated init-system.sh) echo "" -echo "👤 Initializing system (Admin account)..." -chmod +x init-system.sh -./init-system.sh +echo "👤 Creating Initial Administrator..." +API_URL="http://localhost:3001" +# Start the CMS service temporarily to create the user? +# Better yet, start all services with docker compose +echo "🚀 Starting services to allow admin creation..." +docker compose up -d --build +echo "⏳ Waiting for CMS API to be ready..." +START_WAIT=$SECONDS +until curl -s "$API_URL/auth/login" &> /dev/null || [ $((SECONDS - START_WAIT)) -gt 60 ]; do sleep 2; done + +read -p "Admin Email [admin@example.com]: " ADMIN_EMAIL +ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com} +read -s -p "Admin Password [password123]: " ADMIN_PASS +ADMIN_PASS=${ADMIN_PASS:-password123} +echo "" + +PAYLOAD=$(jq -n \ + --arg email "$ADMIN_EMAIL" \ + --arg password "$ADMIN_PASS" \ + --arg full_name "System Admin" \ + --arg org_name "Default Organization" \ + --arg role "admin" \ + '{email: $email, password: $password, full_name: $full_name, organization_name: $org_name, role: $role}') + +RESPONSE=$(curl -s -X POST "$API_URL/auth/register" -H "Content-Type: application/json" -d "$PAYLOAD") + +if echo "$RESPONSE" | grep -q "token"; then + echo "✅ Success! Administrator created." +else + echo "⚠️ Failed to create administrator (it might already exist)." +fi echo "" echo "====================================================" echo " ✨ OpenCCB Installation Complete!" echo "====================================================" -echo "You can now start the services using 'docker compose up' or by" -echo "running 'npm run dev' inside the frontend directories and" -echo "'cargo run' inside the service directories." -echo "" echo "Studio: http://localhost:3000" echo "Experience: http://localhost:3003" -echo "CMS API: http://localhost:3001" -echo "LMS API: http://localhost:3002" echo "====================================================" diff --git a/roadmap.md b/roadmap.md index 73b3847..c96449d 100644 --- a/roadmap.md +++ b/roadmap.md @@ -7,6 +7,7 @@ - [x] Frontend Initialization (Next.js Studio & Experience) - [x] Dockerization of all services - [x] API Integration (Dashboard <-> CMS Service) +- [x] Unified `install.sh` script with hardware detection & auto-config ## Phase 2: Core CMS Features ✅ - [x] Course Outline Editor (Modules & Lessons) diff --git a/shared/common/src/models.rs b/shared/common/src/models.rs index b7ba7c9..990124a 100644 --- a/shared/common/src/models.rs +++ b/shared/common/src/models.rs @@ -123,6 +123,7 @@ pub struct User { pub full_name: String, pub role: String, // admin, instructor, student pub created_at: DateTime, + pub updated_at: DateTime, } #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] diff --git a/web/studio/src/app/courses/[id]/analytics/page.tsx b/web/studio/src/app/courses/[id]/analytics/page.tsx index d22fa14..4508600 100644 --- a/web/studio/src/app/courses/[id]/analytics/page.tsx +++ b/web/studio/src/app/courses/[id]/analytics/page.tsx @@ -2,7 +2,6 @@ import React, { useState, useEffect } from "react"; import { useParams, useRouter } from "next/navigation"; -import Link from "next/link"; import { cmsApi, Course, CourseAnalytics } from "@/lib/api"; import { useAuth } from "@/context/AuthContext"; import { @@ -83,35 +82,26 @@ export default function AnalyticsPage() { .sort((a, b) => a.average_score - b.average_score); return ( -
- {/* Header */} -
-
+
+
+ {/* Header */} +
- -

{course.title} - Performance Insights

-
- {user?.role} View +
+

+ Course Analytics +

+

Performance insights and student progress for {course?.title}

-
-
- -
-
- Courses - / - {course?.title} -
- -
-
-

{course?.title}

-
- Performance Insights -
+
+ {user?.role} View
@@ -226,7 +216,7 @@ export default function AnalyticsPage() {
- + ); } diff --git a/web/studio/src/app/courses/[id]/calendar/page.tsx b/web/studio/src/app/courses/[id]/calendar/page.tsx index 0c20373..82af9ab 100644 --- a/web/studio/src/app/courses/[id]/calendar/page.tsx +++ b/web/studio/src/app/courses/[id]/calendar/page.tsx @@ -1,21 +1,20 @@ "use client"; - import { useEffect, useState } from "react"; import { cmsApi, Course, Lesson } from "@/lib/api"; -import Link from "next/link"; +import { useRouter } from "next/navigation"; import { - Calendar as CalendarIcon, + Plus, + Calendar, + ArrowLeft, ChevronLeft, ChevronRight, - Layout, - CheckCircle2, - BarChart2, - Settings, + Clock, AlertCircle } from "lucide-react"; import CourseEditorLayout from "@/components/CourseEditorLayout"; export default function CourseCalendarPage({ params }: { params: { id: string } }) { + const router = useRouter(); const [course, setCourse] = useState(null); const [lessons, setLessons] = useState([]); const [loading, setLoading] = useState(true); @@ -56,12 +55,12 @@ export default function CourseCalendarPage({ params }: { params: { id: string } // Padding for first week for (let i = 0; i < firstDay; i++) { - days.push(
); + days.push(
); } // Days of month for (let day = 1; day <= daysInMonth; day++) { - const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; + const dateStr = `${year} -${String(month + 1).padStart(2, '0')} -${String(day).padStart(2, '0')} `; const dayLessons = lessons.filter(l => l.due_date && l.due_date.startsWith(dateStr)); days.push( @@ -71,11 +70,11 @@ export default function CourseCalendarPage({ params }: { params: { id: string } {dayLessons.map(lesson => (
{lesson.title} @@ -92,105 +91,108 @@ export default function CourseCalendarPage({ params }: { params: { id: string } const nextMonth = () => setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1)); const prevMonth = () => setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1)); - if (loading) return
Loading calendar...
; + if (loading) return ( +
+
+
+ ); const monthName = currentDate.toLocaleString('default', { month: 'long' }); const year = currentDate.getFullYear(); return ( -
-
- Courses - / - {course?.title} -
- -
-
-

{course?.title}

-
- - Course Calendar +
+
+ {/* Header */} +
+
+ +
+

+ Course Calendar +

+

Manage important dates and deadlines for {course?.title}

+
-
- - Back to Outline - -
-
- -
-
-
-

{monthName} {year}

-
- - - + +
+
+
+

{monthName} {year}

+
+ + + +
+
+ +
+
+ Exam +
+
+ Assignment +
+
+ Live +
+
+ Lesson +
-
-
- Exam -
-
- Assignment -
-
- Live -
-
- Lesson -
+
+ {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => ( +
+ {day} +
+ ))} + {renderCalendar()}
-
-
- {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => ( -
- {day} -
- ))} - {renderCalendar()} -
- -
-

- - Upcoming Deadlines -

-
- {lessons - .filter(l => l.due_date && new Date(l.due_date) >= new Date()) - .sort((a, b) => new Date(a.due_date!).getTime() - new Date(b.due_date!).getTime()) - .slice(0, 6) - .map(lesson => ( -
-
-
-
- {lesson.important_date_type || 'Activity'} +
+

+ + Upcoming Deadlines +

+
+ {lessons + .filter(l => l.due_date && new Date(l.due_date) >= new Date()) + .sort((a, b) => new Date(a.due_date!).getTime() - new Date(b.due_date!).getTime()) + .slice(0, 6) + .map(lesson => ( +
+
+
+
+ {lesson.important_date_type || 'Activity'} +
+
{lesson.title}
+
+
+
{new Date(lesson.due_date!).toLocaleDateString()}
+
Due Date
-
{lesson.title}
-
-
-
{new Date(lesson.due_date!).toLocaleDateString()}
-
Due Date
-
- )) - } + )) + } +
-
- + +
); } diff --git a/web/studio/src/app/courses/[id]/grading/page.tsx b/web/studio/src/app/courses/[id]/grading/page.tsx index c5950b3..e829e81 100644 --- a/web/studio/src/app/courses/[id]/grading/page.tsx +++ b/web/studio/src/app/courses/[id]/grading/page.tsx @@ -11,12 +11,8 @@ import { CheckCircle2, ArrowLeft, TrendingUp, - Settings, - Layout, - Calendar, - BarChart2 + Settings } from "lucide-react"; -import Link from "next/link"; import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; import CourseEditorLayout from "@/components/CourseEditorLayout"; diff --git a/web/studio/src/app/courses/[id]/page.tsx b/web/studio/src/app/courses/[id]/page.tsx index 338b286..92a7575 100644 --- a/web/studio/src/app/courses/[id]/page.tsx +++ b/web/studio/src/app/courses/[id]/page.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { cmsApi, Course, Module, Lesson } from "@/lib/api"; +import { useRouter } from "next/navigation"; import Link from "next/link"; import { Plus, @@ -11,14 +12,11 @@ import { PlayCircle, FileText, Calendar, - CheckCircle2, - Settings, - BarChart2, - Layout, Save, X, GripVertical, - Trash2 + Trash2, + ArrowLeft } from "lucide-react"; import CourseEditorLayout from "@/components/CourseEditorLayout"; @@ -27,6 +25,7 @@ interface FullModule extends Module { } export default function CourseEditor({ params }: { params: { id: string } }) { + const router = useRouter(); const [course, setCourse] = useState(null); const [modules, setModules] = useState([]); const [loading, setLoading] = useState(true); @@ -192,202 +191,210 @@ export default function CourseEditor({ params }: { params: { id: string } }) { if (error) return
{error}
; return ( -
-
- Courses - / - {course?.title} -
- -
-
-

{course?.title}

-
- Editor - Outline - - {course?.pacing_mode?.replace('_', ' ') || 'Self Paced'} - +
+
+ {/* Header */} +
+
+ +
+

+ Course Editor +

+
+

Design your course structure and lesson content for {course?.title}

+ + {course?.pacing_mode?.replace('_', ' ') || 'Self Paced'} + +
+
+
+
+ +
-
- - -
-
- -
- {modules.map((module, mIndex) => ( -
-
-
-
+ +
+ {modules.map((module, mIndex) => ( +
+
+
+
+ + +
+ + + {editingId === module.id ? ( +
+ setEditValue(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSaveTitle(module.id, 'module')} + className="bg-black/40 border border-blue-500/50 rounded px-3 py-1 flex-1 text-white focus:outline-none" + /> + + +
+ ) : ( +
+ { setEditingId(module.id); setEditValue(module.title); }} + className="font-semibold text-lg text-blue-400 cursor-pointer hover:text-blue-300 transition-colors" + > + {module.title || `Module ${module.position}`} + + +
+ )} +
+
-
- - - {editingId === module.id ? ( -
- setEditValue(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleSaveTitle(module.id, 'module')} - className="bg-black/40 border border-blue-500/50 rounded px-3 py-1 flex-1 text-white focus:outline-none" - /> - - -
- ) : ( -
- { setEditingId(module.id); setEditValue(module.title); }} - className="font-semibold text-lg text-blue-400 cursor-pointer hover:text-blue-300 transition-colors" - > - {module.title || `Module ${module.position}`} - - -
- )}
-
+
+ {module.lessons.map((lesson, lIndex) => ( +
+
+ + +
+ +
+ {editingId === lesson.id ? ( +
+ setEditValue(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSaveTitle(lesson.id, 'lesson')} + className="bg-transparent border-none flex-1 text-white focus:outline-none" + /> + + +
+ ) : ( +
+ +
+ {lesson.content_type === 'video' ? : } +
+
+ { e.preventDefault(); e.stopPropagation(); startEditing(lesson.id, lesson.title); }} + className="font-medium hover:text-blue-400 transition-colors" + > + {lesson.title} + +
+ {lesson.content_type} + {lesson.due_date && ( +
+ + {new Date(lesson.due_date).toLocaleDateString()} +
+ )} +
+
+ +
+ + +
+
+ )} +
+
+ ))} +
-
- {module.lessons.map((lesson, lIndex) => ( -
-
- - -
+ ))} -
- {editingId === lesson.id ? ( -
- setEditValue(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleSaveTitle(lesson.id, 'lesson')} - className="bg-transparent border-none flex-1 text-white focus:outline-none" - /> - - -
- ) : ( -
- -
- {lesson.content_type === 'video' ? : } -
-
- { e.preventDefault(); e.stopPropagation(); startEditing(lesson.id, lesson.title); }} - className="font-medium hover:text-blue-400 transition-colors" - > - {lesson.title} - -
- {lesson.content_type} - {lesson.due_date && ( -
- - {new Date(lesson.due_date).toLocaleDateString()} -
- )} -
-
- -
- - -
-
- )} -
-
- ))} - - -
-
- ))} - - -
-
+ +
+ +
); } diff --git a/web/studio/src/app/courses/[id]/settings/page.tsx b/web/studio/src/app/courses/[id]/settings/page.tsx index e5fb281..e18c5f2 100644 --- a/web/studio/src/app/courses/[id]/settings/page.tsx +++ b/web/studio/src/app/courses/[id]/settings/page.tsx @@ -2,9 +2,8 @@ import React, { useState, useEffect } from "react"; import { useParams, useRouter } from "next/navigation"; -import Link from "next/link"; import { cmsApi, Course } from "@/lib/api"; -import { ArrowLeft, Save, Settings as SettingsIcon, BookOpen, Calendar, Clock, Layout, CheckCircle2 } from "lucide-react"; +import { ArrowLeft, Save, Settings as SettingsIcon, BookOpen, Calendar, Clock } from "lucide-react"; const DEFAULT_CERTIFICATE_TEMPLATE = `
@@ -21,6 +20,8 @@ const DEFAULT_CERTIFICATE_TEMPLATE = `
`; +import CourseEditorLayout from "@/components/CourseEditorLayout"; + export default function CourseSettingsPage() { const { id } = useParams() as { id: string }; const router = useRouter(); @@ -73,239 +74,231 @@ export default function CourseSettingsPage() { }; if (loading) return ( -
-
+
+
); if (!course) return ( -
+
Course not found.
); return ( -
- {/* Header */} -
-
+
+
+ {/* Header */} +
- -

{course.title} - Settings

+
+

+ Course Settings +

+

Configure general course properties and certificates for {course?.title}

+
-
-
-
-
- - Outline - - - Grading - - - Calendar - - - Settings - -
-
+ - {/* Passing Percentage Section */} -
-
-
- -
-

Grading Configuration

-
- -
-
- -
- setPassingPercentage(parseInt(e.target.value))} - className="flex-1 h-2 bg-white/10 rounded-lg appearance-none cursor-pointer accent-blue-500" - /> -
- {passingPercentage}% -
+ {/* Passing Percentage Section */} +
+
+
+
-

- Students must achieve at least this percentage to pass the course. -

+

Grading Configuration

- {/* Performance Tiers Preview */} -
-

Performance Tiers Preview

-
-
-
- Reprobado: - 0% - {Math.max(0, passingPercentage - 1)}% -
-
-
- Rendimiento Bajo: - {passingPercentage}% - {passingPercentage + 9}% -
-
-
- Rendimiento Medio: - {passingPercentage + 10}% - {passingPercentage + 15}% -
-
-
- Buen Rendimiento: - {passingPercentage + 16}% - 90% -
-
-
- Excelente: - 91% - 100% -
-
-
-
-
- - {/* Course Pacing Section */} -
-
-
- -
-

Course Pacing & Schedule

-
- -
-
- -
- - -
-
- - {pacingMode === 'instructor_led' && ( -
- -
-
- -
- - setStartDate(e.target.value)} - className="w-full bg-black/30 border border-white/10 rounded-xl py-2 pl-10 pr-4 text-sm focus:outline-none focus:border-blue-500" - /> -
-
-
- -
- - setEndDate(e.target.value)} - className="w-full bg-black/30 border border-white/10 rounded-xl py-2 pl-10 pr-4 text-sm focus:outline-none focus:border-blue-500" - /> -
-
-
-
- )} -
-
- - {/* Certificate Template Section */} -
-
-
- -
-

Certificate Template

-
- -
-

- Design the HTML certificate that students will receive upon passing the course. - Available variables: {"{{student_name}}"}, {"{{course_title}}"}, {"{{date}}"}, {"{{score}}"}. -

- -
-
- -