feat: Refactor course editor layout to support dynamic page headers and standardize styling with new utility classes, including some localization.
This commit is contained in:
@@ -9,16 +9,15 @@ import {
|
||||
Users,
|
||||
TrendingUp,
|
||||
AlertTriangle,
|
||||
ArrowLeft,
|
||||
CheckCircle2,
|
||||
BookOpen,
|
||||
Layers,
|
||||
ShieldAlert
|
||||
} from "lucide-react";
|
||||
import CourseEditorLayout from "@/components/CourseEditorLayout";
|
||||
import DropoutRiskDashboard from "@/components/Analytics/DropoutRiskDashboard";
|
||||
import LiveSessions from "@/components/Courses/LiveSessions";
|
||||
import { Video } from "lucide-react";
|
||||
import CourseEditorLayout from "@/components/CourseEditorLayout";
|
||||
|
||||
export default function AnalyticsPage() {
|
||||
const { id } = useParams() as { id: string };
|
||||
@@ -34,19 +33,13 @@ export default function AnalyticsPage() {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
// Wait for auth to load
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check authorization
|
||||
if (!user) return;
|
||||
if (user.role !== 'admin' && user.role !== 'instructor') {
|
||||
router.push('/');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch cohorts once
|
||||
const cohortsData = await lmsApi.getCohorts();
|
||||
setCohorts(cohortsData);
|
||||
|
||||
@@ -94,72 +87,57 @@ export default function AnalyticsPage() {
|
||||
.sort((a, b) => a.average_score - b.average_score);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-transparent text-gray-900 dark:text-white p-8">
|
||||
<div className="max-w-7xl 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>
|
||||
<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-600 dark:text-gray-400 mt-1">Performance insights and student progress for {course?.title}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<CourseEditorLayout
|
||||
activeTab="analytics"
|
||||
pageTitle="Análisis del Curso"
|
||||
pageDescription={`Insights de rendimiento y progreso para ${course?.title}`}
|
||||
pageActions={
|
||||
<div className="flex items-center gap-3">
|
||||
<select
|
||||
value={selectedCohortId}
|
||||
onChange={(e) => setSelectedCohortId(e.target.value)}
|
||||
className="bg-black/5 dark:bg-white/5 text-gray-900 dark:text-white border border-black/10 dark:border-white/10 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
||||
className="bg-white/5 text-gray-900 dark:text-white border border-white/10 rounded-xl px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 min-w-[160px]"
|
||||
>
|
||||
<option value="">All Students</option>
|
||||
<option value="">Todos los Estudiantes</option>
|
||||
{cohorts.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||
</select>
|
||||
<button
|
||||
onClick={() => router.push(`/courses/${id}/analytics/advanced`)}
|
||||
className="btn-premium !bg-purple-600/10 !text-purple-400 border border-purple-500/20 hover:!bg-purple-600/20 !shadow-none gap-2 text-xs py-2"
|
||||
className="flex items-center gap-2 px-4 py-2 bg-purple-600/10 hover:bg-purple-600/20 text-purple-400 border border-purple-500/20 rounded-xl font-bold text-xs transition-all active:scale-95"
|
||||
>
|
||||
<Layers size={14} /> Advanced Insights
|
||||
<Layers size={14} /> Insights Avanzados
|
||||
</button>
|
||||
<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/10 text-blue-400 text-[10px] font-black uppercase tracking-wider px-3 py-1.5 rounded-lg border border-blue-500/20">
|
||||
{user?.role} View
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CourseEditorLayout activeTab="analytics">
|
||||
<div className="p-8">
|
||||
}
|
||||
>
|
||||
<div className="space-y-12">
|
||||
{/* Tab Selector */}
|
||||
<div className="flex items-center gap-1 mb-10 p-1 bg-white/5 rounded-2xl w-fit">
|
||||
<button
|
||||
onClick={() => setActiveAnalyticsTab("overview")}
|
||||
className={`px-6 py-2.5 rounded-xl text-sm font-bold transition-all flex items-center gap-2 ${activeAnalyticsTab === "overview" ? 'bg-blue-600 text-gray-900 dark:text-white shadow-lg shadow-blue-500/20' : 'text-gray-400 hover:text-gray-900 dark:text-white hover:bg-white/5'}`}
|
||||
className={`px-6 py-2.5 rounded-xl text-sm font-bold transition-all flex items-center gap-2 ${activeAnalyticsTab === "overview" ? "bg-blue-600 text-white shadow-lg shadow-blue-500/20" : "text-gray-400 hover:text-white hover:bg-white/5"}`}
|
||||
>
|
||||
<BarChart3 size={16} /> Overview
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveAnalyticsTab("risks")}
|
||||
className={`px-6 py-2.5 rounded-xl text-sm font-bold transition-all flex items-center gap-2 ${activeAnalyticsTab === "risks" ? 'bg-red-600 text-gray-900 dark:text-white shadow-lg shadow-red-500/20' : 'text-gray-400 hover:text-gray-900 dark:text-white hover:bg-white/5'}`}
|
||||
className={`px-6 py-2.5 rounded-xl text-sm font-bold transition-all flex items-center gap-2 ${activeAnalyticsTab === "risks" ? "bg-red-600 text-white shadow-lg shadow-red-500/20" : "text-gray-400 hover:text-white hover:bg-white/5"}`}
|
||||
>
|
||||
<ShieldAlert size={16} /> Predictive Risks
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveAnalyticsTab("live")}
|
||||
className={`px-6 py-2.5 rounded-xl text-sm font-bold transition-all flex items-center gap-2 ${activeAnalyticsTab === "live" ? 'bg-blue-600 text-gray-900 dark:text-white shadow-lg shadow-blue-500/20' : 'text-gray-400 hover:text-gray-900 dark:text-white hover:bg-white/5'}`}
|
||||
className={`px-6 py-2.5 rounded-xl text-sm font-bold transition-all flex items-center gap-2 ${activeAnalyticsTab === "live" ? "bg-blue-600 text-white shadow-lg shadow-blue-500/20" : "text-gray-400 hover:text-white hover:bg-white/5"}`}
|
||||
>
|
||||
<Video size={16} /> Live Sessions
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeAnalyticsTab === "overview" ? (
|
||||
{activeAnalyticsTab === "overview" && (
|
||||
<div className="space-y-12 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="bg-white/5 border border-white/10 rounded-3xl p-8 group hover:bg-white/[0.07] transition-all">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
@@ -196,11 +174,10 @@ export default function AnalyticsPage() {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
{/* Lesson Breakdown */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-black mb-6 flex items-center gap-3">
|
||||
<h2 className="section-title mb-6">
|
||||
<BarChart3 className="text-blue-500" />
|
||||
Lesson Performance
|
||||
Rendimiento de Lecciones
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{analytics.lessons.map((lesson) => (
|
||||
@@ -210,13 +187,13 @@ export default function AnalyticsPage() {
|
||||
<h3 className="font-bold">{lesson.lesson_title}</h3>
|
||||
<p className="text-xs text-gray-500 mt-1">{lesson.submission_count} submissions</p>
|
||||
</div>
|
||||
<div className={`text-xl font-black ${lesson.average_score < 0.6 ? 'text-red-400' : lesson.average_score < 0.8 ? 'text-orange-400' : 'text-green-400'}`}>
|
||||
<div className={`text-xl font-black ${lesson.average_score < 0.6 ? "text-red-400" : lesson.average_score < 0.8 ? "text-orange-400" : "text-green-400"}`}>
|
||||
{Math.round(lesson.average_score * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-1.5 bg-white/5 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-1000 ${lesson.average_score < 0.6 ? 'bg-red-500' : lesson.average_score < 0.8 ? 'bg-orange-500' : 'bg-green-500'}`}
|
||||
className={`h-full rounded-full transition-all duration-1000 ${lesson.average_score < 0.6 ? "bg-red-500" : lesson.average_score < 0.8 ? "bg-orange-500" : "bg-green-500"}`}
|
||||
style={{ width: `${lesson.average_score * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -225,12 +202,11 @@ export default function AnalyticsPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Actionable Insights */}
|
||||
<section className="space-y-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-black mb-6 flex items-center gap-3">
|
||||
<h2 className="section-title mb-6">
|
||||
<AlertTriangle className="text-orange-500" />
|
||||
Struggling Lessons
|
||||
Lecciones con Dificultad
|
||||
</h2>
|
||||
{difficultLessons.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
@@ -254,7 +230,6 @@ export default function AnalyticsPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-600/10 border border-blue-500/20 rounded-3xl p-8">
|
||||
<h3 className="text-lg font-bold mb-4 flex items-center gap-2">
|
||||
<BookOpen className="text-blue-400" />
|
||||
@@ -267,12 +242,16 @@ export default function AnalyticsPage() {
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
)}
|
||||
|
||||
{activeAnalyticsTab === "risks" && (
|
||||
<DropoutRiskDashboard courseId={id} />
|
||||
)}
|
||||
|
||||
{activeAnalyticsTab === "live" && (
|
||||
<LiveSessions courseId={id} />
|
||||
)}
|
||||
</div>
|
||||
</CourseEditorLayout>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function AnnouncementsPage() {
|
||||
lmsApi.getCohorts()
|
||||
]);
|
||||
setAnnouncements(annData);
|
||||
setCohorts(cohortData.filter(c => c.course_id === id));
|
||||
setCohorts(cohortData.filter((c: any) => c.course_id === id));
|
||||
} catch (error) {
|
||||
console.error("Error fetching announcements:", error);
|
||||
} finally {
|
||||
@@ -54,35 +54,26 @@ export default function AnnouncementsPage() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-transparent text-gray-900 dark:text-white p-8">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<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>
|
||||
<h1 className="text-3xl font-bold bg-gradient-to-r from-orange-400 to-red-400 bg-clip-text text-transparent">
|
||||
Announcements
|
||||
</h1>
|
||||
<p className="text-gray-400 mt-1">Manage course communications and cohort segments</p>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<CourseEditorLayout
|
||||
activeTab="announcements"
|
||||
pageTitle="Anuncios"
|
||||
pageDescription="Gestiona las comunicaciones del curso y segmentos de cohortes."
|
||||
pageActions={
|
||||
<button
|
||||
onClick={() => setShowNewModal(true)}
|
||||
className="flex items-center gap-2 px-6 py-3 bg-orange-600 hover:bg-orange-500 rounded-xl font-bold shadow-lg shadow-orange-500/20 transition-all active:scale-95"
|
||||
className="flex items-center gap-2 px-6 py-2.5 bg-orange-600 hover:bg-orange-500 text-white rounded-xl font-bold text-sm shadow-md shadow-orange-500/20 transition-all active:scale-95"
|
||||
>
|
||||
<Plus size={18} />
|
||||
New Announcement
|
||||
Nuevo Anuncio
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<CourseEditorLayout activeTab="announcements">
|
||||
<div className="space-y-6">
|
||||
}
|
||||
>
|
||||
<div className="space-y-8">
|
||||
<h2 className="section-title">
|
||||
<Megaphone className="text-orange-500" />
|
||||
Comunicados del Curso
|
||||
</h2>
|
||||
{/* Search Bar */}
|
||||
<div className="glass p-4 rounded-2xl flex items-center gap-4">
|
||||
<div className="relative flex-1">
|
||||
@@ -170,9 +161,9 @@ export default function AnnouncementsPage() {
|
||||
)}
|
||||
</div>
|
||||
</CourseEditorLayout>
|
||||
</div>
|
||||
|
||||
{showNewModal && (
|
||||
{
|
||||
showNewModal && (
|
||||
<NewAnnouncementModal
|
||||
courseId={id}
|
||||
cohorts={cohorts}
|
||||
@@ -183,7 +174,7 @@ export default function AnnouncementsPage() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -98,56 +98,40 @@ export default function CourseCalendarPage({ params }: { params: { id: string }
|
||||
const year = currentDate.getFullYear();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-transparent text-gray-900 dark: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"
|
||||
<CourseEditorLayout
|
||||
activeTab="calendar"
|
||||
pageTitle="Calendario del Curso"
|
||||
pageDescription={`Gestiona fechas importantes y plazos para ${course?.title || 'este curso'}.`}
|
||||
>
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<div>
|
||||
<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>
|
||||
|
||||
<CourseEditorLayout activeTab="calendar">
|
||||
<div className="p-8">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="flex items-center gap-6">
|
||||
<h3 className="text-2xl font-black uppercase tracking-tight">{monthName} <span className="text-blue-500">{year}</span></h3>
|
||||
<div className="flex items-center gap-2 bg-white/5 rounded-xl p-1 border border-white/10">
|
||||
<button onClick={prevMonth} className="p-2 hover:bg-white/10 rounded-lg transition-colors"><ChevronLeft className="w-5 h-5" /></button>
|
||||
<button onClick={() => setCurrentDate(new Date())} className="px-3 py-1 text-xs font-bold uppercase tracking-widest hover:text-blue-400 transition-colors">Today</button>
|
||||
<button onClick={() => setCurrentDate(new Date())} className="px-3 py-1 text-xs font-bold uppercase tracking-widest hover:text-blue-400 transition-colors">Hoy</button>
|
||||
<button onClick={nextMonth} className="p-2 hover:bg-white/10 rounded-lg transition-colors"><ChevronRight className="w-5 h-5" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
||||
<span className="w-2 h-2 rounded-full bg-red-500"></span> Exam
|
||||
<span className="w-2 h-2 rounded-full bg-red-500"></span> Examen
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500"></span> Assignment
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500"></span> Tarea
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
||||
<span className="w-2 h-2 rounded-full bg-purple-500"></span> Live
|
||||
<span className="w-2 h-2 rounded-full bg-purple-500"></span> En Vivo
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
||||
<span className="w-2 h-2 rounded-full bg-green-500"></span> Lesson
|
||||
<span className="w-2 h-2 rounded-full bg-green-500"></span> Lección
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-7 border-t border-l border-white/5 rounded-xl overflow-hidden shadow-2xl overflow-hidden">
|
||||
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
|
||||
{['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'].map(day => (
|
||||
<div key={day} className="bg-white/5 py-4 text-center text-xs font-black uppercase tracking-widest text-gray-500 border-r border-b border-white/5">
|
||||
{day}
|
||||
</div>
|
||||
@@ -189,7 +173,5 @@ export default function CourseCalendarPage({ params }: { params: { id: string }
|
||||
</div>
|
||||
</div>
|
||||
</CourseEditorLayout>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { lmsApi, cmsApi, StudentGradeReport, Cohort } from "@/lib/api";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import CourseEditorLayout from "@/components/CourseEditorLayout";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Search,
|
||||
GraduationCap,
|
||||
Download,
|
||||
@@ -142,39 +141,28 @@ export default function GradebookPage() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-transparent text-gray-900 dark:text-white p-8">
|
||||
<div className="max-w-7xl 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>
|
||||
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent">
|
||||
Gradebook
|
||||
</h1>
|
||||
<p className="text-gray-400 mt-1">Student, progress, and performance tracking</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<CourseEditorLayout
|
||||
activeTab="grades"
|
||||
pageTitle="Libro de Calificaciones"
|
||||
pageDescription="Seguimiento del progreso, calificaciones y rendimiento de los estudiantes."
|
||||
pageActions={
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => setShowBulkEnroll(true)}
|
||||
className="bg-white/5 hover:bg-white/10 border border-white/10 px-4 py-2 rounded-xl flex items-center gap-2 transition-all font-medium"
|
||||
className="bg-white/5 hover:bg-white/10 border border-white/10 px-4 py-2 rounded-xl flex items-center gap-2 transition-all font-medium text-sm"
|
||||
>
|
||||
<Users size={16} /> Bulk Enroll
|
||||
<Users size={16} /> Inscripción Masiva
|
||||
</button>
|
||||
<button
|
||||
onClick={exportCSV}
|
||||
className="btn-premium px-4 flex items-center gap-2"
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white rounded-xl font-bold text-sm shadow-md shadow-blue-600/20 transition-all active:scale-95"
|
||||
>
|
||||
<Download size={16} /> Export CSV
|
||||
<Download size={16} /> Exportar CSV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<>
|
||||
|
||||
{/* Bulk Enroll Modal */}
|
||||
{showBulkEnroll && (
|
||||
@@ -182,7 +170,7 @@ export default function GradebookPage() {
|
||||
<div className="bg-[#1a1d23] border border-white/10 rounded-2xl w-full max-w-2xl overflow-hidden shadow-2xl animate-in fade-in zoom-in duration-200">
|
||||
<div className="p-6 border-b border-white/5 flex items-center justify-between">
|
||||
<h3 className="text-xl font-bold flex items-center gap-2">
|
||||
<Users className="text-blue-400" /> Bulk Student Enrollment
|
||||
<Users className="text-blue-400" /> Inscripción Masiva de Estudiantes
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -201,7 +189,7 @@ export default function GradebookPage() {
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-2">
|
||||
Enter email addresses (separated by commas or new lines)
|
||||
Ingresa las direcciones de correo (separadas por comas o saltos de línea)
|
||||
</label>
|
||||
<textarea
|
||||
value={bulkEmails}
|
||||
@@ -212,7 +200,7 @@ export default function GradebookPage() {
|
||||
</div>
|
||||
<div className="bg-blue-500/10 border border-blue-500/20 rounded-xl p-4 flex gap-3 italic text-sm text-blue-400">
|
||||
<AlertTriangle size={18} className="shrink-0" />
|
||||
Students must already have an account in this organization to be enrolled.
|
||||
Los estudiantes deben tener una cuenta en esta organización para ser inscritos.
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
@@ -260,7 +248,7 @@ export default function GradebookPage() {
|
||||
disabled={bulkLoading || !bulkEmails.trim()}
|
||||
className="btn-premium px-8 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{bulkLoading ? <Loader2 className="animate-spin w-4 h-4" /> : 'Process Enrollment'}
|
||||
{bulkLoading ? <Loader2 className="animate-spin w-4 h-4" /> : 'Procesar Inscripción'}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
@@ -280,8 +268,11 @@ export default function GradebookPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CourseEditorLayout activeTab="grades">
|
||||
<div className="p-8 space-y-8">
|
||||
<div className="space-y-8">
|
||||
<h2 className="section-title">
|
||||
<Users className="text-blue-500" />
|
||||
Registro de Calificaciones
|
||||
</h2>
|
||||
{/* Controls & Stats */}
|
||||
<div className="flex flex-col md:flex-row gap-6 justify-between items-center bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-4 w-full md:w-auto">
|
||||
@@ -405,8 +396,7 @@ export default function GradebookPage() {
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</CourseEditorLayout>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,8 +79,8 @@ export default function TeamManagementSection({ courseId }: TeamManagementSectio
|
||||
<Users size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-black">Course Team</h2>
|
||||
<p className="text-sm text-gray-400">Manage instructors and assistants for this course</p>
|
||||
<h2 className="section-title">Equipo del Curso</h2>
|
||||
<p className="text-sm text-gray-400">Gestiona los instructores y asistentes de este curso</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { cmsApi, Course } from "@/lib/api";
|
||||
import { ArrowLeft, Save, Settings as SettingsIcon, BookOpen, Calendar, Clock, Download, Upload } from "lucide-react";
|
||||
import { Save, Settings as SettingsIcon, BookOpen, Calendar, Clock, Download, Upload } 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;">
|
||||
@@ -129,42 +129,22 @@ export default function CourseSettingsPage() {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!course) return (
|
||||
<div className="min-h-screen bg-transparent text-gray-900 dark:text-white p-20 text-center">
|
||||
Course not found.
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-transparent text-gray-900 dark:text-white p-8">
|
||||
<div className="max-w-5xl 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>
|
||||
<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>
|
||||
<CourseEditorLayout
|
||||
activeTab="settings"
|
||||
pageTitle="Configuración del Curso"
|
||||
pageDescription="Configura las propiedades generales del curso y las plantillas de certificados."
|
||||
pageActions={
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
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" : ""}`}
|
||||
className={`flex items-center gap-2 px-6 py-2.5 bg-blue-600 hover:bg-blue-500 text-white rounded-xl font-bold text-sm shadow-md shadow-blue-600/20 transition-all active:scale-95 ${saving ? "opacity-75 cursor-wait" : ""}`}
|
||||
>
|
||||
<Save size={18} />
|
||||
{saving ? "Saving..." : "Save Changes"}
|
||||
{saving ? "Guardando..." : "Guardar Cambios"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<CourseEditorLayout activeTab="settings">
|
||||
}
|
||||
>
|
||||
<div className="space-y-8">
|
||||
<TeamManagementSection courseId={id} />
|
||||
|
||||
@@ -174,7 +154,7 @@ export default function CourseSettingsPage() {
|
||||
<div className="w-12 h-12 rounded-2xl bg-purple-500/10 flex items-center justify-center text-purple-400">
|
||||
<SettingsIcon size={24} />
|
||||
</div>
|
||||
<h2 className="text-2xl font-black">Grading Configuration</h2>
|
||||
<h2 className="section-title">Configuración de Calificaciones</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
@@ -240,7 +220,7 @@ export default function CourseSettingsPage() {
|
||||
<div className="w-12 h-12 rounded-2xl bg-green-500/10 flex items-center justify-center text-green-400">
|
||||
<Clock size={24} />
|
||||
</div>
|
||||
<h2 className="text-2xl font-black">Course Pacing & Schedule</h2>
|
||||
<h2 className="section-title">Ritmo y Calendario del Curso</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
@@ -304,7 +284,7 @@ export default function CourseSettingsPage() {
|
||||
<div className="w-12 h-12 rounded-2xl bg-blue-500/10 flex items-center justify-center text-blue-400">
|
||||
<span className="text-xl font-bold">$</span>
|
||||
</div>
|
||||
<h2 className="text-2xl font-black">Course Pricing</h2>
|
||||
<h2 className="section-title">Precio del Curso</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
@@ -351,7 +331,7 @@ export default function CourseSettingsPage() {
|
||||
<div className="w-12 h-12 rounded-2xl bg-indigo-500/10 flex items-center justify-center text-indigo-400">
|
||||
<BookOpen size={24} />
|
||||
</div>
|
||||
<h2 className="text-2xl font-black">Certificate Template</h2>
|
||||
<h2 className="section-title">Plantilla de Certificado</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
@@ -402,7 +382,7 @@ export default function CourseSettingsPage() {
|
||||
<div className="w-12 h-12 rounded-2xl bg-amber-500/10 flex items-center justify-center text-amber-400">
|
||||
<Download size={24} />
|
||||
</div>
|
||||
<h2 className="text-2xl font-black">Course Portability</h2>
|
||||
<h2 className="section-title">Portabilidad del Curso</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
@@ -449,7 +429,5 @@ export default function CourseSettingsPage() {
|
||||
</section>
|
||||
</div>
|
||||
</CourseEditorLayout>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,31 +116,26 @@ export default function CourseStudentsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-transparent text-gray-900 dark:text-white p-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<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>
|
||||
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent">
|
||||
Students & Groups
|
||||
</h1>
|
||||
<p className="text-gray-400 mt-1">Manage enrollments and segment your audience</p>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<CourseEditorLayout
|
||||
activeTab="students"
|
||||
pageTitle="Estudiantes y Grupos"
|
||||
pageDescription="Gestiona las inscripciones y segmenta a tu audiencia por cohortes."
|
||||
pageActions={
|
||||
<button
|
||||
onClick={() => setIsEnrollModalOpen(true)}
|
||||
className="btn-premium px-6 py-3 flex items-center gap-2"
|
||||
className="flex items-center gap-2 px-6 py-2.5 bg-blue-600 hover:bg-blue-500 text-white rounded-xl font-bold text-sm shadow-md shadow-blue-600/20 transition-all active:scale-95"
|
||||
>
|
||||
<UserPlus size={18} />
|
||||
Enroll Students
|
||||
Inscribir Estudiantes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<CourseEditorLayout activeTab="students">
|
||||
<div className="space-y-6">
|
||||
}
|
||||
>
|
||||
<div className="space-y-8">
|
||||
<h2 className="section-title">
|
||||
<Users className="text-blue-500" />
|
||||
Listado de Estudiantes
|
||||
</h2>
|
||||
{/* Search and Filters */}
|
||||
<div className="glass p-4 rounded-2xl flex flex-col md:flex-row items-center gap-4">
|
||||
<div className="relative flex-1 w-full">
|
||||
@@ -232,10 +227,10 @@ export default function CourseStudentsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</CourseEditorLayout>
|
||||
</div>
|
||||
|
||||
{/* Enroll Modal */}
|
||||
{isEnrollModalOpen && (
|
||||
{
|
||||
isEnrollModalOpen && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200">
|
||||
<div className="bg-[#16181b] border border-white/10 rounded-3xl w-full max-w-2xl overflow-hidden shadow-2xl scale-in-center">
|
||||
<div className="p-6 border-b border-white/5 flex items-center justify-between">
|
||||
@@ -295,6 +290,6 @@ export default function CourseStudentsPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,21 +96,25 @@ export default function CourseTeamPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto p-8">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Course Team</h1>
|
||||
<p className="text-gray-400 mt-1">Manage multiple instructors and assistants for this course</p>
|
||||
</div>
|
||||
<>
|
||||
<CourseEditorLayout
|
||||
activeTab="team"
|
||||
pageTitle="Equipo del Curso"
|
||||
pageDescription="Gestiona múltiples instructores y asistentes para este curso."
|
||||
pageActions={
|
||||
<button
|
||||
onClick={() => setIsAddModalOpen(true)}
|
||||
className="btn-premium px-6 py-2.5 flex items-center gap-2 group"
|
||||
className="flex items-center gap-2 px-6 py-2.5 bg-blue-600 hover:bg-blue-500 text-white rounded-xl font-bold text-sm shadow-md shadow-blue-600/20 transition-all active:scale-95 group"
|
||||
>
|
||||
<UserPlus className="w-4 h-4 group-hover:scale-110 transition-transform" /> Add Member
|
||||
<UserPlus className="w-4 h-4 group-hover:scale-110 transition-transform" /> Agregar Miembro
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<CourseEditorLayout activeTab="team">
|
||||
}
|
||||
>
|
||||
<div className="space-y-8">
|
||||
<h2 className="section-title">
|
||||
<ShieldCheck className="text-blue-500" />
|
||||
Miembros del Equipo
|
||||
</h2>
|
||||
<div className="grid gap-4">
|
||||
{loading ? (
|
||||
<div className="py-20 text-center text-gray-500 animate-pulse">Loading team members...</div>
|
||||
@@ -161,10 +165,12 @@ export default function CourseTeamPage() {
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CourseEditorLayout>
|
||||
|
||||
{/* Add Member Modal */}
|
||||
{isAddModalOpen && (
|
||||
{
|
||||
isAddModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-300">
|
||||
<div className="bg-[#1a1c22] border border-white/10 rounded-3xl w-full max-w-lg overflow-hidden shadow-2xl animate-in zoom-in-95 duration-300">
|
||||
<div className="p-8 border-b border-white/5 flex justify-between items-center">
|
||||
@@ -252,6 +258,6 @@ export default function CourseTeamPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,30 @@ body {
|
||||
@apply scale-95;
|
||||
}
|
||||
|
||||
/* Premium Typography System */
|
||||
.heading-premium {
|
||||
@apply text-3xl md:text-4xl font-black tracking-tight leading-tight;
|
||||
background: linear-gradient(to bottom right, #111827, #374151);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.dark .heading-premium {
|
||||
background: linear-gradient(to bottom right, #ffffff, #9ca3af);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@apply text-xl md:text-2xl font-black tracking-tight flex items-center gap-3 text-gray-900 dark:text-white;
|
||||
}
|
||||
|
||||
.text-description-premium {
|
||||
@apply text-sm md:text-base text-slate-500 dark:text-gray-400 font-medium;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
|
||||
@@ -141,13 +141,13 @@ export default function CourseEditorLayout({
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-gray-900 dark:text-white tracking-tight leading-tight">
|
||||
<h1 className="heading-premium">
|
||||
{displayTitle}
|
||||
</h1>
|
||||
<p className="text-sm text-slate-500 dark:text-gray-400 mt-0.5">
|
||||
<p className="text-description-premium mt-1.5 flex items-center gap-2">
|
||||
{displayDescription}
|
||||
{course?.pacing_mode && (
|
||||
<span className={`ml-2 text-xs font-bold px-1.5 py-0.5 rounded ${course.pacing_mode === "instructor_led"
|
||||
<span className={`text-[10px] font-black px-2 py-0.5 rounded-full uppercase tracking-widest ${course.pacing_mode === "instructor_led"
|
||||
? "bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-400"
|
||||
: "bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-400"
|
||||
}`}>
|
||||
|
||||
Reference in New Issue
Block a user