feat: enhance install script with hardware detection, refactor studio UI, and enable GPU support for whisper service

This commit is contained in:
2025-12-29 23:49:21 -03:00
parent ad56d8a81c
commit 6326cad39d
12 changed files with 678 additions and 856 deletions
-68
View File
@@ -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
+9 -12
View File
@@ -57,24 +57,21 @@ services:
NEXT_PUBLIC_LMS_API_URL: http://localhost:3002 NEXT_PUBLIC_LMS_API_URL: http://localhost:3002
whisper: whisper:
image: fedirz/faster-whisper-server:latest-cpu image: ${WHISPER_IMAGE:-fedirz/faster-whisper-server:latest-cpu}
ports: ports:
- "8000:8000" - "8000:8000"
volumes: volumes:
- whisper_cache:/root/.cache/huggingface - whisper_cache:/root/.cache/huggingface
environment: environment:
# - WHISPER_MODEL=medium - DEVICE=${WHISPER_DEVICE:-cpu}
# - DEVICE=cpu
# GPU support commented out for stability if drivers missing
- DEVICE=cpu
# GPU support for RTX 2070 Super # GPU support for RTX 2070 Super
# deploy: deploy:
# resources: resources:
# reservations: reservations:
# devices: devices:
# - driver: nvidia - driver: nvidia
# count: 1 count: 1
# capabilities: [ gpu ] capabilities: [ gpu ]
e2e: e2e:
build: build:
-78
View File
@@ -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
+107 -137
View File
@@ -1,14 +1,15 @@
#!/bin/bash #!/bin/bash
# OpenCCB Unified Installation Script # OpenCCB Unified Installation Script
# This script automates the setup of OpenCCB, including prerequisites, # This script automates the setup of OpenCCB:
# repository cloning, dependencies, and initial configuration. # 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 set -e
REPO_URL="https://github.com/Nurfog/openccb.git" # Example URL, should be updated if needed
PROJECT_DIR="openccb"
echo "====================================================" echo "===================================================="
echo " 🚀 Welcome to the OpenCCB Installer" echo " 🚀 Welcome to the OpenCCB Installer"
echo "====================================================" echo "===================================================="
@@ -19,24 +20,18 @@ if [ -f "Cargo.toml" ] && [ -d "services" ] && [ -d "web" ]; then
echo "✅ Project detected in current directory." echo "✅ Project detected in current directory."
PROJECT_ROOT=$(pwd) PROJECT_ROOT=$(pwd)
else else
echo "📂 Project not detected in current directory." # Simplification: assume we are in the project root if the script is running
if [ -d "$PROJECT_DIR" ]; then # but let's keep a basic check
echo "✅ Detected project folder '$PROJECT_DIR'." if [ -d "openccb" ]; then
cd "$PROJECT_DIR" cd openccb
PROJECT_ROOT=$(pwd) PROJECT_ROOT=$(pwd)
else else
echo "📥 Project folder not found. Cloning from $REPO_URL..." echo "⚠️ Please run this script from the root of the OpenCCB repository."
git clone "$REPO_URL" "$PROJECT_DIR" exit 1
cd "$PROJECT_DIR"
PROJECT_ROOT=$(pwd)
fi fi
fi fi
# 2. Prerequisite Installation # 2. Prerequisite Installation
echo ""
echo "🔍 Checking for prerequisites..."
# Function to check and install system packages (Ubuntu/Debian)
install_pkg() { install_pkg() {
if ! command -v "$1" &> /dev/null; then if ! command -v "$1" &> /dev/null; then
echo "🔧 Installing $1..." echo "🔧 Installing $1..."
@@ -46,7 +41,6 @@ install_pkg() {
fi fi
} }
# Check for essential tools
if [[ "$OSTYPE" == "linux-gnu"* ]]; then if [[ "$OSTYPE" == "linux-gnu"* ]]; then
if [ -f /etc/debian_version ]; then if [ -f /etc/debian_version ]; then
install_pkg "curl" install_pkg "curl"
@@ -54,191 +48,167 @@ if [[ "$OSTYPE" == "linux-gnu"* ]]; then
install_pkg "jq" install_pkg "jq"
install_pkg "build-essential" install_pkg "build-essential"
install_pkg "docker.io" 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 if ! docker compose version &> /dev/null; then
install_pkg "docker-compose-v2" install_pkg "docker-compose-v2"
fi fi
else
echo "⚠️ Unsupported Linux distribution. Please ensure curl, git, jq, docker, and docker-compose are installed."
fi fi
fi fi
# Check for Rust
if ! command -v cargo &> /dev/null; then if ! command -v cargo &> /dev/null; then
echo "🔧 Installing Rust..." echo "🔧 Installing Rust..."
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source $HOME/.cargo/env source $HOME/.cargo/env
else
echo "✅ Rust (Cargo) is already installed."
fi fi
# Check for Node.js
if ! command -v node &> /dev/null; then 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 curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
export NVM_DIR="$HOME/.nvm" export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install --lts nvm install --lts
else
echo "✅ Node.js $(node -v) is already installed."
fi fi
# Check for sqlx-cli
if ! command -v sqlx &> /dev/null; then if ! command -v sqlx &> /dev/null; then
echo "🔧 Installing sqlx-cli..." echo "🔧 Installing sqlx-cli..."
cargo install sqlx-cli --no-default-features --features postgres cargo install sqlx-cli --no-default-features --features postgres
else
echo "✅ sqlx-cli is already installed."
fi fi
# AI Stack Detection & Installation # 3. Hardware Detection
echo "" echo ""
echo "🤖 Setting up Local AI Stack..." echo "🔍 Detecting hardware..."
HAS_NVIDIA=false HAS_NVIDIA=false
if command -v nvidia-smi &> /dev/null; then if command -v nvidia-smi &> /dev/null && nvidia-smi -L &> /dev/null; then
if nvidia-smi -L &> /dev/null; then
echo "🚀 NVIDIA GPU Detected!" echo "🚀 NVIDIA GPU Detected!"
HAS_NVIDIA=true HAS_NVIDIA=true
fi elif command -v lspci &> /dev/null && lspci | grep -i nvidia &> /dev/null; then
fi
if [ "$HAS_NVIDIA" = false ] && command -v lspci &> /dev/null; then
if lspci | grep -i nvidia &> /dev/null; then
echo "🚀 NVIDIA GPU Detected (lspci)!" echo "🚀 NVIDIA GPU Detected (lspci)!"
HAS_NVIDIA=true 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
else else
echo "✅ Ollama is already installed." echo "💻 No NVIDIA GPU found. Using CPU mode."
fi 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 # 4. Environment Configuration
echo "" echo ""
echo "⚙️ Configuring environment..."
if [ ! -f ".env" ]; then if [ ! -f ".env" ]; then
if [ -f ".env.example" ]; then if [ -f ".env.example" ]; then
echo "📄 Creating .env from .env.example..."
cp .env.example .env cp .env.example .env
else else
echo "📄 Creating a new .env file..."
touch .env touch .env
fi fi
fi fi
# Function to update or add a variable in .env
update_env() { update_env() {
local key=$1 local key=$1
local default_value=$2 local val=$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}
if grep -q "^${key}=" .env; then if grep -q "^${key}=" .env; then
# Use a temporary file for sed to be safe sed -i "s|^${key}=.*|${key}=${val}|" .env
sed -i "s|^${key}=.*|${key}=${user_val}|" .env
else else
echo "${key}=${user_val}" >> .env echo "${key}=${val}" >> .env
fi fi
} }
echo "Please provide the following configuration values (Press Enter for default):" # Auto-configure AI variables based on hardware
update_env "DATABASE_URL" "postgresql://user:password@localhost:5432/openccb" "Master Database URL" if [ "$HAS_NVIDIA" = true ]; then
update_env "CMS_DATABASE_URL" "postgresql://user:password@localhost:5432/openccb_cms" "CMS Database URL" update_env "WHISPER_IMAGE" "fedirz/faster-whisper-server:latest-cuda"
update_env "LMS_DATABASE_URL" "postgresql://user:password@localhost:5432/openccb_lms" "LMS Database URL" update_env "WHISPER_DEVICE" "cuda"
update_env "NEXT_PUBLIC_CMS_API_URL" "http://localhost:3001" "Studio CMS API URL" update_env "LOCAL_LLM_MODEL" "llama3:8b"
update_env "NEXT_PUBLIC_LMS_API_URL" "http://localhost:3002" "Experience LMS API URL" # Uncomment GPU deploy section in docker-compose.yml while preserving indentation
sed -i '/deploy:/s/# //' docker-compose.yml
echo "" sed -i '/resources:/s/# //' docker-compose.yml
echo "🛠️ AI Configuration..." sed -i '/reservations:/s/# //' docker-compose.yml
update_env "AI_PROVIDER" "local" "AI Provider (openai | local)" sed -i '/devices:/s/# //' docker-compose.yml
if [ "$(grep "^AI_PROVIDER=" .env | cut -d'=' -f2)" == "local" ]; then sed -i '/- driver: nvidia/s/# //' docker-compose.yml
update_env "LOCAL_OLLAMA_URL" "http://localhost:11434" "Local Ollama API URL" sed -i '/count: 1/s/# //' docker-compose.yml
update_env "LOCAL_WHISPER_URL" "http://localhost:8000" "Local Whisper API URL" sed -i '/capabilities: \[ gpu \]/s/# //' docker-compose.yml
default_model="phi3:mini"
if [ "$HAS_NVIDIA" = true ]; then
default_model="llama3:8b"
fi
update_env "LOCAL_LLM_MODEL" "$default_model" "Local LLM Model"
else 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 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 ""
echo "🐘 Starting database with Docker..." echo "🐘 Starting database with Docker..."
docker compose up -d db docker compose up -d db
sleep 5 # Simple wait
echo "⏳ Waiting for database to be ready..." CMS_URL=$(grep "CMS_DATABASE_URL=" .env | cut -d'=' -f2)
# Better wait using pg_isready if available LMS_URL=$(grep "LMS_DATABASE_URL=" .env | cut -d'=' -f2)
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
echo "🏗️ Running database setup..." echo "🏗️ Creating databases and running migrations..."
chmod +x db-mgmt.sh DATABASE_URL=$CMS_URL sqlx database create || true
./db-mgmt.sh setup 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 ""
echo "👤 Initializing system (Admin account)..." echo "👤 Creating Initial Administrator..."
chmod +x init-system.sh API_URL="http://localhost:3001"
./init-system.sh # 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 "====================================================" echo "===================================================="
echo " ✨ OpenCCB Installation Complete!" echo " ✨ OpenCCB Installation Complete!"
echo "====================================================" 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 "Studio: http://localhost:3000"
echo "Experience: http://localhost:3003" echo "Experience: http://localhost:3003"
echo "CMS API: http://localhost:3001"
echo "LMS API: http://localhost:3002"
echo "====================================================" echo "===================================================="
+1
View File
@@ -7,6 +7,7 @@
- [x] Frontend Initialization (Next.js Studio & Experience) - [x] Frontend Initialization (Next.js Studio & Experience)
- [x] Dockerization of all services - [x] Dockerization of all services
- [x] API Integration (Dashboard <-> CMS Service) - [x] API Integration (Dashboard <-> CMS Service)
- [x] Unified `install.sh` script with hardware detection & auto-config
## Phase 2: Core CMS Features ✅ ## Phase 2: Core CMS Features ✅
- [x] Course Outline Editor (Modules & Lessons) - [x] Course Outline Editor (Modules & Lessons)
+1
View File
@@ -123,6 +123,7 @@ pub struct User {
pub full_name: String, pub full_name: String,
pub role: String, // admin, instructor, student pub role: String, // admin, instructor, student
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
} }
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
@@ -2,7 +2,6 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import Link from "next/link";
import { cmsApi, Course, CourseAnalytics } from "@/lib/api"; import { cmsApi, Course, CourseAnalytics } from "@/lib/api";
import { useAuth } from "@/context/AuthContext"; import { useAuth } from "@/context/AuthContext";
import { import {
@@ -83,37 +82,28 @@ export default function AnalyticsPage() {
.sort((a, b) => a.average_score - b.average_score); .sort((a, b) => a.average_score - b.average_score);
return ( return (
<div className="min-h-screen bg-gray-950 text-white pb-20"> <div className="min-h-screen bg-[#0f1115] text-white p-8">
<div className="max-w-7xl mx-auto">
{/* Header */} {/* Header */}
<header className="sticky top-0 z-50 bg-gray-950/80 backdrop-blur-xl border-b border-white/5 py-4 px-8"> <div className="flex items-center justify-between mb-12">
<div className="max-w-7xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<button onClick={() => router.back()} className="p-2 hover:bg-white/5 rounded-full transition-colors"> <button
<ArrowLeft className="w-5 h-5 text-gray-400" /> onClick={() => router.back()}
className="p-2 hover:bg-white/10 rounded-full transition-colors"
>
<ArrowLeft className="w-6 h-6" />
</button> </button>
<h1 className="text-xl font-bold">{course.title} - Performance Insights</h1> <div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent">
Course Analytics
</h1>
<p className="text-gray-400 mt-1">Performance insights and student progress for {course?.title}</p>
</div>
</div>
<div className="bg-blue-500/20 text-blue-400 text-[10px] font-black uppercase tracking-wider px-2 py-1 rounded border border-blue-500/30"> <div className="bg-blue-500/20 text-blue-400 text-[10px] font-black uppercase tracking-wider px-2 py-1 rounded border border-blue-500/30">
{user?.role} View {user?.role} View
</div> </div>
</div> </div>
</div>
</header>
<main className="max-w-7xl mx-auto px-8 mt-12 space-y-8">
<div className="flex items-center gap-4 text-sm text-gray-400">
<Link href="/" className="hover:text-white transition-colors">Courses</Link>
<span>/</span>
<span className="text-white">{course?.title}</span>
</div>
<div className="flex justify-between items-center">
<div>
<h2 className="text-3xl font-bold">{course?.title}</h2>
<div className="flex items-center gap-3 mt-1">
<span className="text-gray-400 text-sm">Performance Insights</span>
</div>
</div>
</div>
<CourseEditorLayout activeTab="analytics"> <CourseEditorLayout activeTab="analytics">
<div className="p-8 space-y-12"> <div className="p-8 space-y-12">
@@ -226,7 +216,7 @@ export default function AnalyticsPage() {
</div> </div>
</div> </div>
</CourseEditorLayout> </CourseEditorLayout>
</main> </div>
</div> </div>
); );
} }
@@ -1,21 +1,20 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { cmsApi, Course, Lesson } from "@/lib/api"; import { cmsApi, Course, Lesson } from "@/lib/api";
import Link from "next/link"; import { useRouter } from "next/navigation";
import { import {
Calendar as CalendarIcon, Plus,
Calendar,
ArrowLeft,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
Layout, Clock,
CheckCircle2,
BarChart2,
Settings,
AlertCircle AlertCircle
} from "lucide-react"; } from "lucide-react";
import CourseEditorLayout from "@/components/CourseEditorLayout"; import CourseEditorLayout from "@/components/CourseEditorLayout";
export default function CourseCalendarPage({ params }: { params: { id: string } }) { export default function CourseCalendarPage({ params }: { params: { id: string } }) {
const router = useRouter();
const [course, setCourse] = useState<Course | null>(null); const [course, setCourse] = useState<Course | null>(null);
const [lessons, setLessons] = useState<Lesson[]>([]); const [lessons, setLessons] = useState<Lesson[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -56,12 +55,12 @@ export default function CourseCalendarPage({ params }: { params: { id: string }
// Padding for first week // Padding for first week
for (let i = 0; i < firstDay; i++) { for (let i = 0; i < firstDay; i++) {
days.push(<div key={`empty-${i}`} className="h-32 border border-white/5 bg-white/2"></div>); days.push(<div key={`empty - ${i} `} className="h-32 border border-white/5 bg-white/2"></div>);
} }
// Days of month // Days of month
for (let day = 1; day <= daysInMonth; day++) { 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)); const dayLessons = lessons.filter(l => l.due_date && l.due_date.startsWith(dateStr));
days.push( days.push(
@@ -71,11 +70,11 @@ export default function CourseCalendarPage({ params }: { params: { id: string }
{dayLessons.map(lesson => ( {dayLessons.map(lesson => (
<div <div
key={lesson.id} key={lesson.id}
className={`text-[10px] p-1 rounded truncate flex items-center gap-1 ${lesson.important_date_type === 'exam' ? 'bg-red-500/20 text-red-400 border border-red-500/30' : className={`text - [10px] p - 1 rounded truncate flex items - center gap - 1 ${lesson.important_date_type === 'exam' ? 'bg-red-500/20 text-red-400 border border-red-500/30' :
lesson.important_date_type === 'assignment' ? 'bg-blue-500/20 text-blue-400 border border-blue-500/30' : lesson.important_date_type === 'assignment' ? 'bg-blue-500/20 text-blue-400 border border-blue-500/30' :
lesson.important_date_type === 'live-session' ? 'bg-purple-500/20 text-purple-400 border border-purple-500/30' : lesson.important_date_type === 'live-session' ? 'bg-purple-500/20 text-purple-400 border border-purple-500/30' :
'bg-green-500/20 text-green-400 border border-green-500/30' 'bg-green-500/20 text-green-400 border border-green-500/30'
}`} } `}
> >
<span className="w-1.5 h-1.5 rounded-full bg-current"></span> <span className="w-1.5 h-1.5 rounded-full bg-current"></span>
{lesson.title} {lesson.title}
@@ -92,32 +91,34 @@ export default function CourseCalendarPage({ params }: { params: { id: string }
const nextMonth = () => setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1)); const nextMonth = () => setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1));
const prevMonth = () => setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1)); const prevMonth = () => setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1));
if (loading) return <div className="py-20 text-center">Loading calendar...</div>; if (loading) return (
<div className="min-h-screen bg-[#0f1115] flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
</div>
);
const monthName = currentDate.toLocaleString('default', { month: 'long' }); const monthName = currentDate.toLocaleString('default', { month: 'long' });
const year = currentDate.getFullYear(); const year = currentDate.getFullYear();
return ( return (
<div className="space-y-8"> <div className="min-h-screen bg-[#0f1115] text-white p-8">
<div className="flex items-center gap-4 text-sm text-gray-400"> <div className="max-w-6xl mx-auto">
<Link href="/" className="hover:text-white transition-colors">Courses</Link> {/* Header */}
<span>/</span> <div className="flex items-center justify-between mb-12">
<span className="text-white">{course?.title}</span> <div className="flex items-center gap-4">
</div> <button
onClick={() => router.back()}
<div className="flex justify-between items-center"> className="p-2 hover:bg-white/10 rounded-full transition-colors"
>
<ArrowLeft className="w-6 h-6" />
</button>
<div> <div>
<h2 className="text-3xl font-bold">{course?.title}</h2> <h1 className="text-3xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent">
<div className="flex items-center gap-3 mt-1 text-gray-400 text-sm"> Course Calendar
<CalendarIcon className="w-4 h-4" /> </h1>
<span>Course Calendar</span> <p className="text-gray-400 mt-1">Manage important dates and deadlines for {course?.title}</p>
</div> </div>
</div> </div>
<div className="flex gap-3">
<Link href={`/courses/${params.id}`} className="px-4 py-2 glass hover:bg-white/10 transition-colors text-sm font-medium">
Back to Outline
</Link>
</div>
</div> </div>
<CourseEditorLayout activeTab="calendar"> <CourseEditorLayout activeTab="calendar">
@@ -171,10 +172,10 @@ export default function CourseCalendarPage({ params }: { params: { id: string }
<div key={lesson.id} className="glass p-4 border-white/5 hover:border-blue-500/30 transition-all group"> <div key={lesson.id} className="glass p-4 border-white/5 hover:border-blue-500/30 transition-all group">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div> <div>
<div className={`text-[10px] font-black uppercase tracking-widest mb-1 ${lesson.important_date_type === 'exam' ? 'text-red-400' : <div className={`text - [10px] font - black uppercase tracking - widest mb - 1 ${lesson.important_date_type === 'exam' ? 'text-red-400' :
lesson.important_date_type === 'assignment' ? 'text-blue-400' : lesson.important_date_type === 'assignment' ? 'text-blue-400' :
'text-green-400' 'text-green-400'
}`}> } `}>
{lesson.important_date_type || 'Activity'} {lesson.important_date_type || 'Activity'}
</div> </div>
<h5 className="font-bold group-hover:text-blue-400 transition-colors">{lesson.title}</h5> <h5 className="font-bold group-hover:text-blue-400 transition-colors">{lesson.title}</h5>
@@ -192,5 +193,6 @@ export default function CourseCalendarPage({ params }: { params: { id: string }
</div> </div>
</CourseEditorLayout> </CourseEditorLayout>
</div> </div>
</div>
); );
} }
@@ -11,12 +11,8 @@ import {
CheckCircle2, CheckCircle2,
ArrowLeft, ArrowLeft,
TrendingUp, TrendingUp,
Settings, Settings
Layout,
Calendar,
BarChart2
} from "lucide-react"; } from "lucide-react";
import Link from "next/link";
import { clsx, type ClassValue } from "clsx"; import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import CourseEditorLayout from "@/components/CourseEditorLayout"; import CourseEditorLayout from "@/components/CourseEditorLayout";
+24 -17
View File
@@ -2,6 +2,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { cmsApi, Course, Module, Lesson } from "@/lib/api"; import { cmsApi, Course, Module, Lesson } from "@/lib/api";
import { useRouter } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { import {
Plus, Plus,
@@ -11,14 +12,11 @@ import {
PlayCircle, PlayCircle,
FileText, FileText,
Calendar, Calendar,
CheckCircle2,
Settings,
BarChart2,
Layout,
Save, Save,
X, X,
GripVertical, GripVertical,
Trash2 Trash2,
ArrowLeft
} from "lucide-react"; } from "lucide-react";
import CourseEditorLayout from "@/components/CourseEditorLayout"; import CourseEditorLayout from "@/components/CourseEditorLayout";
@@ -27,6 +25,7 @@ interface FullModule extends Module {
} }
export default function CourseEditor({ params }: { params: { id: string } }) { export default function CourseEditor({ params }: { params: { id: string } }) {
const router = useRouter();
const [course, setCourse] = useState<Course | null>(null); const [course, setCourse] = useState<Course | null>(null);
const [modules, setModules] = useState<FullModule[]>([]); const [modules, setModules] = useState<FullModule[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -192,32 +191,39 @@ export default function CourseEditor({ params }: { params: { id: string } }) {
if (error) return <div className="py-20 text-center text-red-400">{error}</div>; if (error) return <div className="py-20 text-center text-red-400">{error}</div>;
return ( return (
<div className="space-y-8"> <div className="min-h-screen bg-[#0f1115] text-white p-8">
<div className="flex items-center gap-4 text-sm text-gray-400"> <div className="max-w-6xl mx-auto">
<Link href="/" className="hover:text-white transition-colors">Courses</Link> {/* Header */}
<span>/</span> <div className="flex items-center justify-between mb-12">
<span className="text-white">{course?.title}</span> <div className="flex items-center gap-4">
</div> <button
onClick={() => router.push('/')}
<div className="flex justify-between items-center"> className="p-2 hover:bg-white/10 rounded-full transition-colors"
>
<ArrowLeft className="w-6 h-6" />
</button>
<div> <div>
<h2 className="text-3xl font-bold">{course?.title}</h2> <h1 className="text-3xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent">
Course Editor
</h1>
<div className="flex items-center gap-3 mt-1"> <div className="flex items-center gap-3 mt-1">
<span className="text-gray-400 text-sm">Editor - Outline</span> <p className="text-gray-400">Design your course structure and lesson content for {course?.title}</p>
<span className={`text-[10px] uppercase font-bold px-2 py-0.5 rounded ${course?.pacing_mode === 'instructor_led' ? 'bg-purple-500/20 text-purple-400' : 'bg-green-500/20 text-green-400'}`}> <span className={`text-[10px] uppercase font-bold px-2 py-0.5 rounded ${course?.pacing_mode === 'instructor_led' ? 'bg-purple-500/20 text-purple-400' : 'bg-green-500/20 text-green-400'}`}>
{course?.pacing_mode?.replace('_', ' ') || 'Self Paced'} {course?.pacing_mode?.replace('_', ' ') || 'Self Paced'}
</span> </span>
</div> </div>
</div> </div>
</div>
<div className="flex gap-3"> <div className="flex gap-3">
<button className="flex items-center gap-2 px-4 py-2 glass hover:bg-white/10 transition-colors text-sm font-medium"> <button className="flex items-center gap-2 px-6 py-3 glass hover:bg-white/20 transition-all rounded-xl text-sm font-bold shadow-lg active:scale-95">
Preview Preview
</button> </button>
<button <button
onClick={handlePublish} onClick={handlePublish}
disabled={isPublishing} disabled={isPublishing}
className={`btn-primary flex items-center gap-2 ${isPublishing ? "opacity-75 cursor-wait" : ""}`} className={`flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 rounded-xl text-sm font-bold shadow-lg shadow-blue-500/20 transition-all active:scale-95 ${isPublishing ? "opacity-75 cursor-wait" : ""}`}
> >
<PlayCircle className="w-5 h-5 transition-transform group-active:scale-90" />
{isPublishing ? "Publishing..." : "Publish to LMS"} {isPublishing ? "Publishing..." : "Publish to LMS"}
</button> </button>
</div> </div>
@@ -389,5 +395,6 @@ export default function CourseEditor({ params }: { params: { id: string } }) {
</div> </div>
</CourseEditorLayout> </CourseEditorLayout>
</div> </div>
</div>
); );
} }
@@ -2,9 +2,8 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import Link from "next/link";
import { cmsApi, Course } from "@/lib/api"; 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 = ` const DEFAULT_CERTIFICATE_TEMPLATE = `
<div style="width: 800px; height: 600px; padding: 40px; text-align: center; border: 10px solid #787878; font-family: 'Times New Roman', serif; background-color: #fff; color: #333;"> <div style="width: 800px; height: 600px; padding: 40px; text-align: center; border: 10px solid #787878; font-family: 'Times New Roman', serif; background-color: #fff; color: #333;">
@@ -21,6 +20,8 @@ const DEFAULT_CERTIFICATE_TEMPLATE = `
</div> </div>
`; `;
import CourseEditorLayout from "@/components/CourseEditorLayout";
export default function CourseSettingsPage() { export default function CourseSettingsPage() {
const { id } = useParams() as { id: string }; const { id } = useParams() as { id: string };
const router = useRouter(); const router = useRouter();
@@ -73,56 +74,47 @@ export default function CourseSettingsPage() {
}; };
if (loading) return ( if (loading) return (
<div className="min-h-screen bg-gray-900 flex items-center justify-center"> <div className="min-h-screen bg-[#0f1115] flex items-center justify-center">
<div className="w-12 h-12 border-4 border-blue-500/20 border-t-blue-500 rounded-full animate-spin"></div> <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
</div> </div>
); );
if (!course) return ( if (!course) return (
<div className="min-h-screen bg-gray-900 text-white p-20 text-center"> <div className="min-h-screen bg-[#0f1115] text-white p-20 text-center">
Course not found. Course not found.
</div> </div>
); );
return ( return (
<div className="min-h-screen bg-gray-950 text-white pb-20"> <div className="min-h-screen bg-[#0f1115] text-white p-8">
<div className="max-w-5xl mx-auto">
{/* Header */} {/* Header */}
<header className="sticky top-0 z-50 bg-gray-950/80 backdrop-blur-xl border-b border-white/5 py-4 px-8"> <div className="flex items-center justify-between mb-12">
<div className="max-w-5xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<button onClick={() => router.back()} className="p-2 hover:bg-white/5 rounded-full transition-colors"> <button
<ArrowLeft className="w-5 h-5 text-gray-400" /> onClick={() => router.back()}
className="p-2 hover:bg-white/10 rounded-full transition-colors"
>
<ArrowLeft className="w-6 h-6" />
</button> </button>
<h1 className="text-xl font-bold">{course.title} - Settings</h1> <div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent">
Course Settings
</h1>
<p className="text-gray-400 mt-1">Configure general course properties and certificates for {course?.title}</p>
</div>
</div> </div>
<button <button
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={saving}
className="flex items-center gap-2 px-6 py-2 bg-blue-600 hover:bg-blue-700 rounded-xl font-bold transition-colors disabled:opacity-50" className={`flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 rounded-xl font-bold shadow-lg shadow-blue-500/20 transition-all active:scale-95 ${saving ? "opacity-75 cursor-wait" : ""}`}
> >
<Save size={18} /> <Save size={18} />
{saving ? "Saving..." : "Save Changes"} {saving ? "Saving..." : "Save Changes"}
</button> </button>
</div> </div>
</header>
<main className="max-w-5xl mx-auto px-8 mt-12 space-y-8"> <CourseEditorLayout activeTab="settings">
<div className="glass p-1 mb-12">
<div className="flex border-b border-white/10">
<Link href={`/courses/${id}`} className="flex items-center gap-2 px-6 py-4 text-sm font-medium text-gray-500 hover:text-white transition-colors">
<Layout className="w-4 h-4" /> Outline
</Link>
<Link href={`/courses/${id}/grading`} className="flex items-center gap-2 px-6 py-4 text-sm font-medium text-gray-500 hover:text-white transition-colors">
<CheckCircle2 className="w-4 h-4" /> Grading
</Link>
<Link href={`/courses/${id}/calendar`} className="flex items-center gap-2 px-6 py-4 text-sm font-medium text-gray-500 hover:text-white transition-colors">
<Calendar className="w-4 h-4" /> Calendar
</Link>
<Link href={`/courses/${id}/settings`} className="flex items-center gap-2 px-6 py-4 text-sm font-medium border-b-2 border-blue-500 bg-white/5">
<SettingsIcon className="w-4 h-4" /> Settings
</Link>
</div>
</div>
{/* Passing Percentage Section */} {/* Passing Percentage Section */}
<section className="bg-white/5 border border-white/10 rounded-3xl p-8"> <section className="bg-white/5 border border-white/10 rounded-3xl p-8">
@@ -305,7 +297,8 @@ export default function CourseSettingsPage() {
</div> </div>
</div> </div>
</section> </section>
</main> </CourseEditorLayout>
</div>
</div> </div>
); );
} }
+16 -5
View File
@@ -43,11 +43,21 @@ export default function StudioDashboard() {
}; };
return ( return (
<div className="p-8"> <div className="min-h-screen bg-[#0f1115] text-white p-8">
<div className="flex justify-between items-center mb-8"> <div className="max-w-7xl mx-auto">
<h1 className="text-3xl font-bold">My Courses</h1> {/* Header */}
<button onClick={handleCreateCourse} className="btn-premium flex items-center gap-2"> <div className="flex justify-between items-center mb-12">
<Plus size={18} /> <div>
<h1 className="text-4xl font-black bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent tracking-tight">
My Courses
</h1>
<p className="text-gray-400 mt-2">Manage and monitor your educational content</p>
</div>
<button
onClick={handleCreateCourse}
className="flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 rounded-xl font-bold shadow-lg shadow-blue-500/20 transition-all active:scale-95"
>
<Plus size={20} />
New Course New Course
</button> </button>
</div> </div>
@@ -84,5 +94,6 @@ export default function StudioDashboard() {
</div> </div>
)} )}
</div> </div>
</div>
); );
} }