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
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:
-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
+106 -136
View File
@@ -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
if command -v nvidia-smi &> /dev/null && 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
elif command -v lspci &> /dev/null && 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
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"
# Auto-configure AI variables based on hardware
if [ "$HAS_NVIDIA" = true ]; then
default_model="llama3:8b"
fi
update_env "LOCAL_LLM_MODEL" "$default_model" "Local LLM Model"
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 "===================================================="
+1
View File
@@ -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)
+1
View File
@@ -123,6 +123,7 @@ pub struct User {
pub full_name: String,
pub role: String, // admin, instructor, student
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
@@ -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,37 +82,28 @@ export default function AnalyticsPage() {
.sort((a, b) => a.average_score - b.average_score);
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 className="sticky top-0 z-50 bg-gray-950/80 backdrop-blur-xl border-b border-white/5 py-4 px-8">
<div className="max-w-7xl mx-auto flex items-center justify-between">
<div className="flex items-center justify-between mb-12">
<div className="flex items-center gap-4">
<button onClick={() => router.back()} className="p-2 hover:bg-white/5 rounded-full transition-colors">
<ArrowLeft className="w-5 h-5 text-gray-400" />
<button
onClick={() => router.back()}
className="p-2 hover:bg-white/10 rounded-full transition-colors"
>
<ArrowLeft className="w-6 h-6" />
</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">
{user?.role} View
</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">
<div className="p-8 space-y-12">
@@ -226,7 +216,7 @@ export default function AnalyticsPage() {
</div>
</div>
</CourseEditorLayout>
</main>
</div>
</div>
);
}
@@ -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<Course | null>(null);
const [lessons, setLessons] = useState<Lesson[]>([]);
const [loading, setLoading] = useState(true);
@@ -92,32 +91,34 @@ 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 <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 year = currentDate.getFullYear();
return (
<div className="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 className="min-h-screen bg-[#0f1115] text-white p-8">
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-12">
<div className="flex items-center gap-4">
<button
onClick={() => router.back()}
className="p-2 hover:bg-white/10 rounded-full transition-colors"
>
<ArrowLeft className="w-6 h-6" />
</button>
<div>
<h2 className="text-3xl font-bold">{course?.title}</h2>
<div className="flex items-center gap-3 mt-1 text-gray-400 text-sm">
<CalendarIcon className="w-4 h-4" />
<span>Course Calendar</span>
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent">
Course Calendar
</h1>
<p className="text-gray-400 mt-1">Manage important dates and deadlines for {course?.title}</p>
</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>
<CourseEditorLayout activeTab="calendar">
@@ -192,5 +193,6 @@ export default function CourseCalendarPage({ params }: { params: { id: string }
</div>
</CourseEditorLayout>
</div>
</div>
);
}
@@ -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";
+24 -17
View File
@@ -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<Course | null>(null);
const [modules, setModules] = useState<FullModule[]>([]);
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>;
return (
<div className="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 className="min-h-screen bg-[#0f1115] text-white p-8">
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-12">
<div className="flex items-center gap-4">
<button
onClick={() => router.push('/')}
className="p-2 hover:bg-white/10 rounded-full transition-colors"
>
<ArrowLeft className="w-6 h-6" />
</button>
<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">
<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'}`}>
{course?.pacing_mode?.replace('_', ' ') || 'Self Paced'}
</span>
</div>
</div>
</div>
<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
</button>
<button
onClick={handlePublish}
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"}
</button>
</div>
@@ -389,5 +395,6 @@ export default function CourseEditor({ params }: { params: { id: string } }) {
</div>
</CourseEditorLayout>
</div>
</div>
);
}
@@ -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 = `
<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>
`;
import CourseEditorLayout from "@/components/CourseEditorLayout";
export default function CourseSettingsPage() {
const { id } = useParams() as { id: string };
const router = useRouter();
@@ -73,56 +74,47 @@ export default function CourseSettingsPage() {
};
if (loading) return (
<div className="min-h-screen bg-gray-900 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="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>
);
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.
</div>
);
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 className="sticky top-0 z-50 bg-gray-950/80 backdrop-blur-xl border-b border-white/5 py-4 px-8">
<div className="max-w-5xl mx-auto flex items-center justify-between">
<div className="flex items-center justify-between mb-12">
<div className="flex items-center gap-4">
<button onClick={() => router.back()} className="p-2 hover:bg-white/5 rounded-full transition-colors">
<ArrowLeft className="w-5 h-5 text-gray-400" />
<button
onClick={() => router.back()}
className="p-2 hover:bg-white/10 rounded-full transition-colors"
>
<ArrowLeft className="w-6 h-6" />
</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>
<button
onClick={handleSave}
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} />
{saving ? "Saving..." : "Save Changes"}
</button>
</div>
</header>
<main className="max-w-5xl mx-auto px-8 mt-12 space-y-8">
<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>
<CourseEditorLayout activeTab="settings">
{/* Passing Percentage Section */}
<section className="bg-white/5 border border-white/10 rounded-3xl p-8">
@@ -305,7 +297,8 @@ export default function CourseSettingsPage() {
</div>
</div>
</section>
</main>
</CourseEditorLayout>
</div>
</div>
);
}
+16 -5
View File
@@ -43,11 +43,21 @@ export default function StudioDashboard() {
};
return (
<div className="p-8">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold">My Courses</h1>
<button onClick={handleCreateCourse} className="btn-premium flex items-center gap-2">
<Plus size={18} />
<div className="min-h-screen bg-[#0f1115] text-white p-8">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="flex justify-between items-center mb-12">
<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
</button>
</div>
@@ -84,5 +94,6 @@ export default function StudioDashboard() {
</div>
)}
</div>
</div>
);
}