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,
|
Users,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
ArrowLeft,
|
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
Layers,
|
Layers,
|
||||||
ShieldAlert
|
ShieldAlert
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import CourseEditorLayout from "@/components/CourseEditorLayout";
|
|
||||||
import DropoutRiskDashboard from "@/components/Analytics/DropoutRiskDashboard";
|
import DropoutRiskDashboard from "@/components/Analytics/DropoutRiskDashboard";
|
||||||
import LiveSessions from "@/components/Courses/LiveSessions";
|
import LiveSessions from "@/components/Courses/LiveSessions";
|
||||||
import { Video } from "lucide-react";
|
import { Video } from "lucide-react";
|
||||||
|
import CourseEditorLayout from "@/components/CourseEditorLayout";
|
||||||
|
|
||||||
export default function AnalyticsPage() {
|
export default function AnalyticsPage() {
|
||||||
const { id } = useParams() as { id: string };
|
const { id } = useParams() as { id: string };
|
||||||
@@ -34,19 +33,13 @@ export default function AnalyticsPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
// Wait for auth to load
|
if (!user) return;
|
||||||
if (!user) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check authorization
|
|
||||||
if (user.role !== 'admin' && user.role !== 'instructor') {
|
if (user.role !== 'admin' && user.role !== 'instructor') {
|
||||||
router.push('/');
|
router.push('/');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch cohorts once
|
|
||||||
const cohortsData = await lmsApi.getCohorts();
|
const cohortsData = await lmsApi.getCohorts();
|
||||||
setCohorts(cohortsData);
|
setCohorts(cohortsData);
|
||||||
|
|
||||||
@@ -94,185 +87,171 @@ 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-transparent text-gray-900 dark:text-white p-8">
|
<CourseEditorLayout
|
||||||
<div className="max-w-7xl mx-auto">
|
activeTab="analytics"
|
||||||
{/* Header */}
|
pageTitle="Análisis del Curso"
|
||||||
<div className="flex items-center justify-between mb-12">
|
pageDescription={`Insights de rendimiento y progreso para ${course?.title}`}
|
||||||
<div className="flex items-center gap-4">
|
pageActions={
|
||||||
<button
|
<div className="flex items-center gap-3">
|
||||||
onClick={() => router.back()}
|
<select
|
||||||
className="p-2 hover:bg-white/10 rounded-full transition-colors"
|
value={selectedCohortId}
|
||||||
>
|
onChange={(e) => setSelectedCohortId(e.target.value)}
|
||||||
<ArrowLeft className="w-6 h-6" />
|
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]"
|
||||||
</button>
|
>
|
||||||
<div>
|
<option value="">Todos los Estudiantes</option>
|
||||||
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent">
|
{cohorts.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||||
Course Analytics
|
</select>
|
||||||
</h1>
|
<button
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">Performance insights and student progress for {course?.title}</p>
|
onClick={() => router.push(`/courses/${id}/analytics/advanced`)}
|
||||||
</div>
|
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"
|
||||||
</div>
|
>
|
||||||
<div className="flex items-center gap-4">
|
<Layers size={14} /> Insights Avanzados
|
||||||
<select
|
</button>
|
||||||
value={selectedCohortId}
|
<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">
|
||||||
onChange={(e) => setSelectedCohortId(e.target.value)}
|
{user?.role} View
|
||||||
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"
|
|
||||||
>
|
|
||||||
<option value="">All Students</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"
|
|
||||||
>
|
|
||||||
<Layers size={14} /> Advanced Insights
|
|
||||||
</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">
|
|
||||||
{user?.role} View
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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-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-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-white shadow-lg shadow-blue-500/20" : "text-gray-400 hover:text-white hover:bg-white/5"}`}
|
||||||
|
>
|
||||||
|
<Video size={16} /> Live Sessions
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<CourseEditorLayout activeTab="analytics">
|
{activeAnalyticsTab === "overview" && (
|
||||||
<div className="p-8">
|
<div className="space-y-12 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||||
{/* Tab Selector */}
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<div className="flex items-center gap-1 mb-10 p-1 bg-white/5 rounded-2xl w-fit">
|
<div className="bg-white/5 border border-white/10 rounded-3xl p-8 group hover:bg-white/[0.07] transition-all">
|
||||||
<button
|
<div className="flex items-center gap-4 mb-4">
|
||||||
onClick={() => setActiveAnalyticsTab("overview")}
|
<div className="w-12 h-12 rounded-2xl bg-blue-500/10 flex items-center justify-center text-blue-400">
|
||||||
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'}`}
|
<Users size={24} />
|
||||||
>
|
</div>
|
||||||
<BarChart3 size={16} /> Overview
|
<span className="text-sm font-bold text-gray-400 uppercase tracking-widest">Enrollments</span>
|
||||||
</button>
|
</div>
|
||||||
<button
|
<div className="text-4xl font-black">{analytics.total_enrollments}</div>
|
||||||
onClick={() => setActiveAnalyticsTab("risks")}
|
<div className="text-xs text-green-400 font-bold mt-2">Active Learners</div>
|
||||||
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'}`}
|
</div>
|
||||||
>
|
|
||||||
<ShieldAlert size={16} /> Predictive Risks
|
<div className="bg-white/5 border border-white/10 rounded-3xl p-8 group hover:bg-white/[0.07] transition-all">
|
||||||
</button>
|
<div className="flex items-center gap-4 mb-4">
|
||||||
<button
|
<div className="w-12 h-12 rounded-2xl bg-purple-500/10 flex items-center justify-center text-purple-400">
|
||||||
onClick={() => setActiveAnalyticsTab("live")}
|
<TrendingUp size={24} />
|
||||||
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'}`}
|
</div>
|
||||||
>
|
<span className="text-sm font-bold text-gray-400 uppercase tracking-widest">Average Score</span>
|
||||||
<Video size={16} /> Live Sessions
|
</div>
|
||||||
</button>
|
<div className="text-4xl font-black">{Math.round(analytics.average_score * 100)}%</div>
|
||||||
|
<div className="text-xs text-gray-500 font-bold mt-2">Across all assessments</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-orange-500/10 flex items-center justify-center text-orange-400">
|
||||||
|
<AlertTriangle size={24} />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-bold text-gray-400 uppercase tracking-widest">Attention Needed</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-4xl font-black">{difficultLessons.length}</div>
|
||||||
|
<div className="text-xs text-orange-400 font-bold mt-2">Struggling Lessons</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeAnalyticsTab === "overview" ? (
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||||
<div className="space-y-12 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
<section>
|
||||||
{/* Stats Grid */}
|
<h2 className="section-title mb-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<BarChart3 className="text-blue-500" />
|
||||||
<div className="bg-white/5 border border-white/10 rounded-3xl p-8 group hover:bg-white/[0.07] transition-all">
|
Rendimiento de Lecciones
|
||||||
<div className="flex items-center gap-4 mb-4">
|
</h2>
|
||||||
<div className="w-12 h-12 rounded-2xl bg-blue-500/10 flex items-center justify-center text-blue-400">
|
<div className="space-y-4">
|
||||||
<Users size={24} />
|
{analytics.lessons.map((lesson) => (
|
||||||
|
<div key={lesson.lesson_id} className="bg-black/5 dark:bg-white/5 border border-black/10 dark:border-white/10 rounded-2xl p-6 hover:bg-black/[0.07] dark:hover:bg-white/[0.07] transition-all">
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<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"}`}>
|
||||||
|
{Math.round(lesson.average_score * 100)}%
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-bold text-gray-400 uppercase tracking-widest">Enrollments</span>
|
<div className="h-1.5 bg-white/5 rounded-full overflow-hidden">
|
||||||
</div>
|
<div
|
||||||
<div className="text-4xl font-black">{analytics.total_enrollments}</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"}`}
|
||||||
<div className="text-xs text-green-400 font-bold mt-2">Active Learners</div>
|
style={{ width: `${lesson.average_score * 100}%` }}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
<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">
|
|
||||||
<div className="w-12 h-12 rounded-2xl bg-purple-500/10 flex items-center justify-center text-purple-400">
|
|
||||||
<TrendingUp size={24} />
|
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-bold text-gray-400 uppercase tracking-widest">Average Score</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-4xl font-black">{Math.round(analytics.average_score * 100)}%</div>
|
))}
|
||||||
<div className="text-xs text-gray-500 font-bold mt-2">Across all assessments</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
<div className="w-12 h-12 rounded-2xl bg-orange-500/10 flex items-center justify-center text-orange-400">
|
|
||||||
<AlertTriangle size={24} />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm font-bold text-gray-400 uppercase tracking-widest">Attention Needed</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-4xl font-black">{difficultLessons.length}</div>
|
|
||||||
<div className="text-xs text-orange-400 font-bold mt-2">Struggling Lessons</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
<section className="space-y-8">
|
||||||
{/* Lesson Breakdown */}
|
<div>
|
||||||
<section>
|
<h2 className="section-title mb-6">
|
||||||
<h2 className="text-2xl font-black mb-6 flex items-center gap-3">
|
<AlertTriangle className="text-orange-500" />
|
||||||
<BarChart3 className="text-blue-500" />
|
Lecciones con Dificultad
|
||||||
Lesson Performance
|
</h2>
|
||||||
</h2>
|
{difficultLessons.length > 0 ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{analytics.lessons.map((lesson) => (
|
{difficultLessons.map(l => (
|
||||||
<div key={lesson.lesson_id} className="bg-black/5 dark:bg-white/5 border border-black/10 dark:border-white/10 rounded-2xl p-6 hover:bg-black/[0.07] dark:hover:bg-white/[0.07] transition-all">
|
<div key={l.lesson_id} className="bg-red-500/10 border border-red-500/20 rounded-2xl p-6 flex items-center justify-between">
|
||||||
<div className="flex items-start justify-between mb-4">
|
<div>
|
||||||
<div>
|
<h4 className="font-bold text-red-400">{l.lesson_title}</h4>
|
||||||
<h3 className="font-bold">{lesson.lesson_title}</h3>
|
<p className="text-xs text-red-300/60 mt-1 text-balance max-w-xs">
|
||||||
<p className="text-xs text-gray-500 mt-1">{lesson.submission_count} submissions</p>
|
Average score is below 70%. Consider reviewing the material or difficulty of questions.
|
||||||
</div>
|
</p>
|
||||||
<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'}`}
|
|
||||||
style={{ width: `${lesson.average_score * 100}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-2xl font-black text-red-500">{Math.round(l.average_score * 100)}%</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
) : (
|
||||||
|
<div className="bg-green-500/10 border border-green-500/20 rounded-2xl p-8 text-center">
|
||||||
{/* Actionable Insights */}
|
<CheckCircle2 size={40} className="text-green-500 mx-auto mb-4" />
|
||||||
<section className="space-y-8">
|
<h4 className="font-bold text-green-400">All set!</h4>
|
||||||
<div>
|
<p className="text-sm text-green-300/60 mt-2">No lessons currently fall below the difficulty threshold.</p>
|
||||||
<h2 className="text-2xl font-black mb-6 flex items-center gap-3">
|
|
||||||
<AlertTriangle className="text-orange-500" />
|
|
||||||
Struggling Lessons
|
|
||||||
</h2>
|
|
||||||
{difficultLessons.length > 0 ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{difficultLessons.map(l => (
|
|
||||||
<div key={l.lesson_id} className="bg-red-500/10 border border-red-500/20 rounded-2xl p-6 flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-bold text-red-400">{l.lesson_title}</h4>
|
|
||||||
<p className="text-xs text-red-300/60 mt-1 text-balance max-w-xs">
|
|
||||||
Average score is below 70%. Consider reviewing the material or difficulty of questions.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-black text-red-500">{Math.round(l.average_score * 100)}%</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="bg-green-500/10 border border-green-500/20 rounded-2xl p-8 text-center">
|
|
||||||
<CheckCircle2 size={40} className="text-green-500 mx-auto mb-4" />
|
|
||||||
<h4 className="font-bold text-green-400">All set!</h4>
|
|
||||||
<p className="text-sm text-green-300/60 mt-2">No lessons currently fall below the difficulty threshold.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</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" />
|
|
||||||
Content Strategy Tip
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-blue-200/70 leading-relaxed">
|
|
||||||
High submission counts with low average scores often indicate that the assessment might be misleading or the prerequisites aren't clearly explained in previous lessons.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</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">
|
||||||
<DropoutRiskDashboard courseId={id} />
|
<BookOpen className="text-blue-400" />
|
||||||
)}
|
Content Strategy Tip
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-blue-200/70 leading-relaxed">
|
||||||
|
High submission counts with low average scores often indicate that the assessment might be misleading or the prerequisites aren't clearly explained in previous lessons.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CourseEditorLayout>
|
)}
|
||||||
|
|
||||||
|
{activeAnalyticsTab === "risks" && (
|
||||||
|
<DropoutRiskDashboard courseId={id} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeAnalyticsTab === "live" && (
|
||||||
|
<LiveSessions courseId={id} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CourseEditorLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default function AnnouncementsPage() {
|
|||||||
lmsApi.getCohorts()
|
lmsApi.getCohorts()
|
||||||
]);
|
]);
|
||||||
setAnnouncements(annData);
|
setAnnouncements(annData);
|
||||||
setCohorts(cohortData.filter(c => c.course_id === id));
|
setCohorts(cohortData.filter((c: any) => c.course_id === id));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching announcements:", error);
|
console.error("Error fetching announcements:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -54,136 +54,127 @@ export default function AnnouncementsPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-transparent text-gray-900 dark:text-white p-8">
|
<>
|
||||||
<div className="max-w-5xl mx-auto">
|
<CourseEditorLayout
|
||||||
{/* Header */}
|
activeTab="announcements"
|
||||||
<div className="flex items-center justify-between mb-8">
|
pageTitle="Anuncios"
|
||||||
<div className="flex items-center gap-4">
|
pageDescription="Gestiona las comunicaciones del curso y segmentos de cohortes."
|
||||||
<button
|
pageActions={
|
||||||
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>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowNewModal(true)}
|
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} />
|
<Plus size={18} />
|
||||||
New Announcement
|
Nuevo Anuncio
|
||||||
</button>
|
</button>
|
||||||
</div>
|
}
|
||||||
|
>
|
||||||
<CourseEditorLayout activeTab="announcements">
|
<div className="space-y-8">
|
||||||
<div className="space-y-6">
|
<h2 className="section-title">
|
||||||
{/* Search Bar */}
|
<Megaphone className="text-orange-500" />
|
||||||
<div className="glass p-4 rounded-2xl flex items-center gap-4">
|
Comunicados del Curso
|
||||||
<div className="relative flex-1">
|
</h2>
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
{/* Search Bar */}
|
||||||
<input
|
<div className="glass p-4 rounded-2xl flex items-center gap-4">
|
||||||
type="text"
|
<div className="relative flex-1">
|
||||||
placeholder="Search announcements..."
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||||
value={searchTerm}
|
<input
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
type="text"
|
||||||
className="w-full bg-black/20 border border-white/10 rounded-xl py-2 pl-10 pr-4 text-sm focus:outline-none focus:border-orange-500/50 transition-all"
|
placeholder="Search announcements..."
|
||||||
/>
|
value={searchTerm}
|
||||||
</div>
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full bg-black/20 border border-white/10 rounded-xl py-2 pl-10 pr-4 text-sm focus:outline-none focus:border-orange-500/50 transition-all"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Announcements List */}
|
{/* Announcements List */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex flex-col items-center justify-center py-20 gap-4">
|
<div className="flex flex-col items-center justify-center py-20 gap-4">
|
||||||
<Loader2 className="w-10 h-10 text-orange-500 animate-spin" />
|
<Loader2 className="w-10 h-10 text-orange-500 animate-spin" />
|
||||||
<p className="text-gray-400">Loading announcements...</p>
|
<p className="text-gray-400">Loading announcements...</p>
|
||||||
</div>
|
</div>
|
||||||
) : filteredAnnouncements.length > 0 ? (
|
) : filteredAnnouncements.length > 0 ? (
|
||||||
<div className="grid gap-6">
|
<div className="grid gap-6">
|
||||||
{filteredAnnouncements.map((a) => (
|
{filteredAnnouncements.map((a) => (
|
||||||
<div key={a.id} className={`relative p-6 rounded-2xl border transition-all duration-300 ${a.is_pinned ? 'bg-orange-500/10 border-orange-500/30' : 'bg-white/5 border-white/10 hover:border-white/20'}`}>
|
<div key={a.id} className={`relative p-6 rounded-2xl border transition-all duration-300 ${a.is_pinned ? 'bg-orange-500/10 border-orange-500/30' : 'bg-white/5 border-white/10 hover:border-white/20'}`}>
|
||||||
<div className="flex items-start justify-between mb-4">
|
<div className="flex items-start justify-between mb-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-orange-500 to-red-500 flex items-center justify-center text-gray-900 dark:text-white font-bold overflow-hidden">
|
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-orange-500 to-red-500 flex items-center justify-center text-gray-900 dark:text-white font-bold overflow-hidden">
|
||||||
{a.author_avatar ? (
|
{a.author_avatar ? (
|
||||||
<img src={a.author_avatar} alt={a.author_name} className="w-full h-full object-cover" />
|
<img src={a.author_avatar} alt={a.author_name} className="w-full h-full object-cover" />
|
||||||
) : (
|
) : (
|
||||||
a.author_name.charAt(0)
|
a.author_name.charAt(0)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-gray-900 dark:text-white">{a.author_name}</h4>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||||
|
<span>{formatDistanceToNow(new Date(a.created_at), { addSuffix: true, locale: es })}</span>
|
||||||
|
{a.cohort_ids && a.cohort_ids.length > 0 && (
|
||||||
|
<>
|
||||||
|
<span>•</span>
|
||||||
|
<div className="flex items-center gap-1 text-blue-400 font-medium">
|
||||||
|
<Users className="w-3 h-3" />
|
||||||
|
<span>{a.cohort_ids.length} Cohorts</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-gray-900 dark:text-white">{a.author_name}</h4>
|
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
|
||||||
<span>{formatDistanceToNow(new Date(a.created_at), { addSuffix: true, locale: es })}</span>
|
|
||||||
{a.cohort_ids && a.cohort_ids.length > 0 && (
|
|
||||||
<>
|
|
||||||
<span>•</span>
|
|
||||||
<div className="flex items-center gap-1 text-blue-400 font-medium">
|
|
||||||
<Users className="w-3 h-3" />
|
|
||||||
<span>{a.cohort_ids.length} Cohorts</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{a.is_pinned && <Pin className="w-4 h-4 text-orange-400 fill-current" />}
|
|
||||||
<button
|
|
||||||
onClick={() => handleDelete(a.id)}
|
|
||||||
className="p-2 rounded-lg hover:bg-red-500/20 text-gray-500 hover:text-red-400 transition-colors"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">{a.title}</h3>
|
<div className="flex items-center gap-2">
|
||||||
<p className="text-gray-300 whitespace-pre-wrap">{a.content}</p>
|
{a.is_pinned && <Pin className="w-4 h-4 text-orange-400 fill-current" />}
|
||||||
|
<button
|
||||||
{/* Display Target Cohort Names if segmented */}
|
onClick={() => handleDelete(a.id)}
|
||||||
{a.cohort_ids && a.cohort_ids.length > 0 && (
|
className="p-2 rounded-lg hover:bg-red-500/20 text-gray-500 hover:text-red-400 transition-colors"
|
||||||
<div className="mt-4 flex flex-wrap gap-2">
|
>
|
||||||
{a.cohort_ids.map(cid => {
|
<Trash2 className="w-4 h-4" />
|
||||||
const cohort = cohorts.find(c => c.id === cid);
|
</button>
|
||||||
return (
|
</div>
|
||||||
<span key={cid} className="px-2 py-1 bg-blue-500/10 border border-blue-500/20 rounded-md text-[10px] text-blue-400 font-bold uppercase tracking-wider">
|
|
||||||
{cohort?.name || 'Unknown Cohort'}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">{a.title}</h3>
|
||||||
</div>
|
<p className="text-gray-300 whitespace-pre-wrap">{a.content}</p>
|
||||||
) : (
|
|
||||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-20 text-center">
|
|
||||||
<Megaphone className="w-12 h-12 text-gray-600 mx-auto mb-4" />
|
|
||||||
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">No announcements found</h3>
|
|
||||||
<p className="text-gray-400">Start by creating a new announcement for your students.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CourseEditorLayout>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showNewModal && (
|
{/* Display Target Cohort Names if segmented */}
|
||||||
<NewAnnouncementModal
|
{a.cohort_ids && a.cohort_ids.length > 0 && (
|
||||||
courseId={id}
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
cohorts={cohorts}
|
{a.cohort_ids.map(cid => {
|
||||||
onClose={() => setShowNewModal(false)}
|
const cohort = cohorts.find(c => c.id === cid);
|
||||||
onSuccess={() => {
|
return (
|
||||||
setShowNewModal(false);
|
<span key={cid} className="px-2 py-1 bg-blue-500/10 border border-blue-500/20 rounded-md text-[10px] text-blue-400 font-bold uppercase tracking-wider">
|
||||||
fetchData();
|
{cohort?.name || 'Unknown Cohort'}
|
||||||
}}
|
</span>
|
||||||
/>
|
);
|
||||||
)}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white/5 border border-white/10 rounded-2xl p-20 text-center">
|
||||||
|
<Megaphone className="w-12 h-12 text-gray-600 mx-auto mb-4" />
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">No announcements found</h3>
|
||||||
|
<p className="text-gray-400">Start by creating a new announcement for your students.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CourseEditorLayout>
|
||||||
|
|
||||||
|
{
|
||||||
|
showNewModal && (
|
||||||
|
<NewAnnouncementModal
|
||||||
|
courseId={id}
|
||||||
|
cohorts={cohorts}
|
||||||
|
onClose={() => setShowNewModal(false)}
|
||||||
|
onSuccess={() => {
|
||||||
|
setShowNewModal(false);
|
||||||
|
fetchData();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,98 +98,80 @@ export default function CourseCalendarPage({ params }: { params: { id: string }
|
|||||||
const year = currentDate.getFullYear();
|
const year = currentDate.getFullYear();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-transparent text-gray-900 dark:text-white p-8">
|
<CourseEditorLayout
|
||||||
<div className="max-w-6xl mx-auto">
|
activeTab="calendar"
|
||||||
{/* Header */}
|
pageTitle="Calendario del Curso"
|
||||||
<div className="flex items-center justify-between mb-12">
|
pageDescription={`Gestiona fechas importantes y plazos para ${course?.title || 'este curso'}.`}
|
||||||
<div className="flex items-center gap-4">
|
>
|
||||||
<button
|
<div className="p-8">
|
||||||
onClick={() => router.back()}
|
<div className="flex items-center justify-between mb-8">
|
||||||
className="p-2 hover:bg-white/10 rounded-full transition-colors"
|
<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>
|
||||||
<ArrowLeft className="w-6 h-6" />
|
<div className="flex items-center gap-2 bg-white/5 rounded-xl p-1 border border-white/10">
|
||||||
</button>
|
<button onClick={prevMonth} className="p-2 hover:bg-white/10 rounded-lg transition-colors"><ChevronLeft className="w-5 h-5" /></button>
|
||||||
<div>
|
<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>
|
||||||
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent">
|
<button onClick={nextMonth} className="p-2 hover:bg-white/10 rounded-lg transition-colors"><ChevronRight className="w-5 h-5" /></button>
|
||||||
Course Calendar
|
</div>
|
||||||
</h1>
|
</div>
|
||||||
<p className="text-gray-400 mt-1">Manage important dates and deadlines for {course?.title}</p>
|
|
||||||
|
<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> 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> 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> 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> Lección
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CourseEditorLayout activeTab="calendar">
|
<div className="grid grid-cols-7 border-t border-l border-white/5 rounded-xl overflow-hidden shadow-2xl overflow-hidden">
|
||||||
<div className="p-8">
|
{['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'].map(day => (
|
||||||
<div className="flex items-center justify-between mb-8">
|
<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">
|
||||||
<div className="flex items-center gap-6">
|
{day}
|
||||||
<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={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
|
|
||||||
</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
|
|
||||||
</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
|
|
||||||
</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
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
{renderCalendar()}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-7 border-t border-l border-white/5 rounded-xl overflow-hidden shadow-2xl overflow-hidden">
|
<div className="mt-12 space-y-4">
|
||||||
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
|
<h4 className="text-lg font-bold flex items-center gap-2">
|
||||||
<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">
|
<AlertCircle className="w-5 h-5 text-blue-500" />
|
||||||
{day}
|
Upcoming Deadlines
|
||||||
</div>
|
</h4>
|
||||||
))}
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{renderCalendar()}
|
{lessons
|
||||||
</div>
|
.filter(l => l.due_date && new Date(l.due_date) >= new Date())
|
||||||
|
.sort((a, b) => new Date(a.due_date!).getTime() - new Date(b.due_date!).getTime())
|
||||||
<div className="mt-12 space-y-4">
|
.slice(0, 6)
|
||||||
<h4 className="text-lg font-bold flex items-center gap-2">
|
.map(lesson => (
|
||||||
<AlertCircle className="w-5 h-5 text-blue-500" />
|
<div key={lesson.id} className="glass p-4 border-white/5 hover:border-blue-500/30 transition-all group">
|
||||||
Upcoming Deadlines
|
<div className="flex justify-between items-start">
|
||||||
</h4>
|
<div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className={`text-[10px] font-black uppercase tracking-widest mb-1 ${lesson.important_date_type === 'exam' ? 'text-red-400' :
|
||||||
{lessons
|
lesson.important_date_type === 'assignment' ? 'text-blue-400' :
|
||||||
.filter(l => l.due_date && new Date(l.due_date) >= new Date())
|
'text-green-400'
|
||||||
.sort((a, b) => new Date(a.due_date!).getTime() - new Date(b.due_date!).getTime())
|
}`}>
|
||||||
.slice(0, 6)
|
{lesson.important_date_type || 'Activity'}
|
||||||
.map(lesson => (
|
|
||||||
<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>
|
|
||||||
<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' :
|
|
||||||
'text-green-400'
|
|
||||||
}`}>
|
|
||||||
{lesson.important_date_type || 'Activity'}
|
|
||||||
</div>
|
|
||||||
<h5 className="font-bold group-hover:text-blue-400 transition-colors">{lesson.title}</h5>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<div className="text-sm font-black">{new Date(lesson.due_date!).toLocaleDateString()}</div>
|
|
||||||
<div className="text-[10px] text-gray-500 uppercase font-bold">Due Date</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h5 className="font-bold group-hover:text-blue-400 transition-colors">{lesson.title}</h5>
|
||||||
</div>
|
</div>
|
||||||
))
|
<div className="text-right">
|
||||||
}
|
<div className="text-sm font-black">{new Date(lesson.due_date!).toLocaleDateString()}</div>
|
||||||
</div>
|
<div className="text-[10px] text-gray-500 uppercase font-bold">Due Date</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</CourseEditorLayout>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CourseEditorLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { lmsApi, cmsApi, StudentGradeReport, Cohort } from "@/lib/api";
|
|||||||
import { useAuth } from "@/context/AuthContext";
|
import { useAuth } from "@/context/AuthContext";
|
||||||
import CourseEditorLayout from "@/components/CourseEditorLayout";
|
import CourseEditorLayout from "@/components/CourseEditorLayout";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
|
||||||
Search,
|
Search,
|
||||||
GraduationCap,
|
GraduationCap,
|
||||||
Download,
|
Download,
|
||||||
@@ -142,39 +141,28 @@ export default function GradebookPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-transparent text-gray-900 dark:text-white p-8">
|
<CourseEditorLayout
|
||||||
<div className="max-w-7xl mx-auto">
|
activeTab="grades"
|
||||||
{/* Header */}
|
pageTitle="Libro de Calificaciones"
|
||||||
<div className="flex items-center justify-between mb-12">
|
pageDescription="Seguimiento del progreso, calificaciones y rendimiento de los estudiantes."
|
||||||
<div className="flex items-center gap-4">
|
pageActions={
|
||||||
<button
|
<div className="flex items-center gap-3">
|
||||||
onClick={() => router.back()}
|
<button
|
||||||
className="p-2 hover:bg-white/10 rounded-full transition-colors"
|
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 text-sm"
|
||||||
<ArrowLeft className="w-6 h-6" />
|
>
|
||||||
</button>
|
<Users size={16} /> Inscripción Masiva
|
||||||
<div>
|
</button>
|
||||||
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent">
|
<button
|
||||||
Gradebook
|
onClick={exportCSV}
|
||||||
</h1>
|
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"
|
||||||
<p className="text-gray-400 mt-1">Student, progress, and performance tracking</p>
|
>
|
||||||
</div>
|
<Download size={16} /> Exportar CSV
|
||||||
</div>
|
</button>
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<Users size={16} /> Bulk Enroll
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={exportCSV}
|
|
||||||
className="btn-premium px-4 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Download size={16} /> Export CSV
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
|
||||||
{/* Bulk Enroll Modal */}
|
{/* Bulk Enroll Modal */}
|
||||||
{showBulkEnroll && (
|
{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="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">
|
<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">
|
<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>
|
</h3>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -201,7 +189,7 @@ export default function GradebookPage() {
|
|||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-400 mb-2">
|
<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>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={bulkEmails}
|
value={bulkEmails}
|
||||||
@@ -212,7 +200,7 @@ export default function GradebookPage() {
|
|||||||
</div>
|
</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">
|
<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" />
|
<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>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -260,7 +248,7 @@ export default function GradebookPage() {
|
|||||||
disabled={bulkLoading || !bulkEmails.trim()}
|
disabled={bulkLoading || !bulkEmails.trim()}
|
||||||
className="btn-premium px-8 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
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>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -280,133 +268,135 @@ export default function GradebookPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CourseEditorLayout activeTab="grades">
|
<div className="space-y-8">
|
||||||
<div className="p-8 space-y-8">
|
<h2 className="section-title">
|
||||||
{/* Controls & Stats */}
|
<Users className="text-blue-500" />
|
||||||
<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">
|
Registro de Calificaciones
|
||||||
<div className="flex items-center gap-4 w-full md:w-auto">
|
</h2>
|
||||||
<div className="relative">
|
{/* Controls & Stats */}
|
||||||
<select
|
<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">
|
||||||
value={selectedCohortId}
|
<div className="flex items-center gap-4 w-full md:w-auto">
|
||||||
onChange={(e) => setSelectedCohortId(e.target.value)}
|
<div className="relative">
|
||||||
className="appearance-none bg-black/20 text-gray-900 dark:text-white border border-white/10 rounded-xl pl-4 pr-10 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 min-w-[200px]"
|
<select
|
||||||
>
|
value={selectedCohortId}
|
||||||
<option value="">All Cohorts</option>
|
onChange={(e) => setSelectedCohortId(e.target.value)}
|
||||||
{cohorts.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
className="appearance-none bg-black/20 text-gray-900 dark:text-white border border-white/10 rounded-xl pl-4 pr-10 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 min-w-[200px]"
|
||||||
</select>
|
>
|
||||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-gray-500">
|
<option value="">All Cohorts</option>
|
||||||
▼
|
{cohorts.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||||
</div>
|
</select>
|
||||||
</div>
|
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-gray-500">
|
||||||
<div className="relative flex-1 md:w-64">
|
▼
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 w-4 h-4" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search students..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="w-full bg-black/20 border border-white/10 rounded-xl pl-10 pr-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 text-gray-900 dark:text-white placeholder-gray-600"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="relative flex-1 md:w-64">
|
||||||
<div className="flex gap-8 border-l border-white/10 pl-8">
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 w-4 h-4" />
|
||||||
<div>
|
<input
|
||||||
<p className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">Students</p>
|
type="text"
|
||||||
<p className="text-2xl font-black">{filteredStudents.length}</p>
|
placeholder="Search students..."
|
||||||
</div>
|
value={searchQuery}
|
||||||
<div>
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
<p className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">Avg Score</p>
|
className="w-full bg-black/20 border border-white/10 rounded-xl pl-10 pr-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 text-gray-900 dark:text-white placeholder-gray-600"
|
||||||
<p className={`text-2xl font-black ${averageScore < 0.7 ? 'text-orange-400' : 'text-green-400'}`}>
|
/>
|
||||||
{(averageScore * 100).toFixed(0)}%
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Student List */}
|
<div className="flex gap-8 border-l border-white/10 pl-8">
|
||||||
<div className="rounded-3xl border border-white/10 bg-white/[0.02] overflow-hidden">
|
<div>
|
||||||
<table className="w-full text-left border-collapse">
|
<p className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">Students</p>
|
||||||
<thead>
|
<p className="text-2xl font-black">{filteredStudents.length}</p>
|
||||||
<tr className="bg-white/5 border-b border-white/5">
|
</div>
|
||||||
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500">Student</th>
|
<div>
|
||||||
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500">Progress</th>
|
<p className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">Avg Score</p>
|
||||||
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500">Avg. Score</th>
|
<p className={`text-2xl font-black ${averageScore < 0.7 ? 'text-orange-400' : 'text-green-400'}`}>
|
||||||
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500">Last Active</th>
|
{(averageScore * 100).toFixed(0)}%
|
||||||
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500 text-right">Status</th>
|
</p>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-white/5">
|
|
||||||
{filteredStudents.length === 0 ? (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={5} className="p-12 text-center text-gray-600 italic">No students found.</td>
|
|
||||||
</tr>
|
|
||||||
) : filteredStudents.map((s) => (
|
|
||||||
<tr key={s.user_id} className="hover:bg-white/[0.02] transition-colors group">
|
|
||||||
<td className="p-6">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-gray-900 dark:text-white font-bold text-sm">
|
|
||||||
{s.full_name.charAt(0)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-bold text-gray-900 dark:text-white group-hover:text-blue-400 transition-colors">{s.full_name}</div>
|
|
||||||
<div className="text-xs text-gray-500 flex items-center gap-1 mt-0.5">
|
|
||||||
<Mail size={10} /> {s.email}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="p-6 align-middle">
|
|
||||||
<div className="w-full max-w-[140px]">
|
|
||||||
<div className="flex justify-between text-xs mb-1 font-bold text-gray-400">
|
|
||||||
<span>{(s.progress * 100).toFixed(0)}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="h-1.5 bg-white/10 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full bg-blue-500 rounded-full"
|
|
||||||
style={{ width: `${s.progress * 100}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="p-6 align-middle">
|
|
||||||
{s.average_score !== null ? (
|
|
||||||
<span className={`font-black ${(s.average_score || 0) >= 0.8 ? 'text-green-400' : (s.average_score || 0) >= 0.6 ? 'text-yellow-400' : 'text-red-400'}`}>
|
|
||||||
{((s.average_score || 0) * 100).toFixed(0)}%
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-gray-600 text-xs italic">No grades</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="p-6 align-middle">
|
|
||||||
<div className="text-sm text-gray-400 flex items-center gap-2">
|
|
||||||
<Clock size={14} />
|
|
||||||
{s.last_active_at ? new Date(s.last_active_at).toLocaleDateString() : 'Never'}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="p-6 text-right align-middle">
|
|
||||||
{s.progress >= 1 ? (
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest bg-green-500/10 text-green-400 border border-green-500/20">
|
|
||||||
<CheckCircle size={12} /> Completed
|
|
||||||
</span>
|
|
||||||
) : s.last_active_at && (Date.now() - new Date(s.last_active_at).getTime() > 7 * 24 * 60 * 60 * 1000) ? (
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest bg-red-500/10 text-red-400 border border-red-500/20">
|
|
||||||
<AlertCircle size={12} /> Inactive
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest bg-blue-500/10 text-blue-400 border border-blue-500/20">
|
|
||||||
<GraduationCap size={12} /> Active
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CourseEditorLayout>
|
|
||||||
</div>
|
{/* Student List */}
|
||||||
</div>
|
<div className="rounded-3xl border border-white/10 bg-white/[0.02] overflow-hidden">
|
||||||
|
<table className="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-white/5 border-b border-white/5">
|
||||||
|
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500">Student</th>
|
||||||
|
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500">Progress</th>
|
||||||
|
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500">Avg. Score</th>
|
||||||
|
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500">Last Active</th>
|
||||||
|
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500 text-right">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-white/5">
|
||||||
|
{filteredStudents.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={5} className="p-12 text-center text-gray-600 italic">No students found.</td>
|
||||||
|
</tr>
|
||||||
|
) : filteredStudents.map((s) => (
|
||||||
|
<tr key={s.user_id} className="hover:bg-white/[0.02] transition-colors group">
|
||||||
|
<td className="p-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-gray-900 dark:text-white font-bold text-sm">
|
||||||
|
{s.full_name.charAt(0)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-bold text-gray-900 dark:text-white group-hover:text-blue-400 transition-colors">{s.full_name}</div>
|
||||||
|
<div className="text-xs text-gray-500 flex items-center gap-1 mt-0.5">
|
||||||
|
<Mail size={10} /> {s.email}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-6 align-middle">
|
||||||
|
<div className="w-full max-w-[140px]">
|
||||||
|
<div className="flex justify-between text-xs mb-1 font-bold text-gray-400">
|
||||||
|
<span>{(s.progress * 100).toFixed(0)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-1.5 bg-white/10 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-blue-500 rounded-full"
|
||||||
|
style={{ width: `${s.progress * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-6 align-middle">
|
||||||
|
{s.average_score !== null ? (
|
||||||
|
<span className={`font-black ${(s.average_score || 0) >= 0.8 ? 'text-green-400' : (s.average_score || 0) >= 0.6 ? 'text-yellow-400' : 'text-red-400'}`}>
|
||||||
|
{((s.average_score || 0) * 100).toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-600 text-xs italic">No grades</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="p-6 align-middle">
|
||||||
|
<div className="text-sm text-gray-400 flex items-center gap-2">
|
||||||
|
<Clock size={14} />
|
||||||
|
{s.last_active_at ? new Date(s.last_active_at).toLocaleDateString() : 'Never'}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="p-6 text-right align-middle">
|
||||||
|
{s.progress >= 1 ? (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest bg-green-500/10 text-green-400 border border-green-500/20">
|
||||||
|
<CheckCircle size={12} /> Completed
|
||||||
|
</span>
|
||||||
|
) : s.last_active_at && (Date.now() - new Date(s.last_active_at).getTime() > 7 * 24 * 60 * 60 * 1000) ? (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest bg-red-500/10 text-red-400 border border-red-500/20">
|
||||||
|
<AlertCircle size={12} /> Inactive
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest bg-blue-500/10 text-blue-400 border border-blue-500/20">
|
||||||
|
<GraduationCap size={12} /> Active
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</CourseEditorLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ export default function TeamManagementSection({ courseId }: TeamManagementSectio
|
|||||||
<Users size={24} />
|
<Users size={24} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-black">Course Team</h2>
|
<h2 className="section-title">Equipo del Curso</h2>
|
||||||
<p className="text-sm text-gray-400">Manage instructors and assistants for this course</p>
|
<p className="text-sm text-gray-400">Gestiona los instructores y asistentes de este curso</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
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 { cmsApi, Course } from "@/lib/api";
|
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 = `
|
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;">
|
||||||
@@ -129,327 +129,305 @@ export default function CourseSettingsPage() {
|
|||||||
</div>
|
</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 (
|
return (
|
||||||
<div className="min-h-screen bg-transparent text-gray-900 dark:text-white p-8">
|
<CourseEditorLayout
|
||||||
<div className="max-w-5xl mx-auto">
|
activeTab="settings"
|
||||||
{/* Header */}
|
pageTitle="Configuración del Curso"
|
||||||
<div className="flex items-center justify-between mb-12">
|
pageDescription="Configura las propiedades generales del curso y las plantillas de certificados."
|
||||||
<div className="flex items-center gap-4">
|
pageActions={
|
||||||
<button
|
<button
|
||||||
onClick={() => router.back()}
|
onClick={handleSave}
|
||||||
className="p-2 hover:bg-white/10 rounded-full transition-colors"
|
disabled={saving}
|
||||||
>
|
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" : ""}`}
|
||||||
<ArrowLeft className="w-6 h-6" />
|
>
|
||||||
</button>
|
<Save size={18} />
|
||||||
|
{saving ? "Guardando..." : "Guardar Cambios"}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="space-y-8">
|
||||||
|
<TeamManagementSection courseId={id} />
|
||||||
|
|
||||||
|
{/* Passing Percentage Section */}
|
||||||
|
<section className="bg-white/5 border border-white/10 rounded-3xl p-8">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<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="section-title">Configuración de Calificaciones</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent">
|
<label className="block text-sm font-bold text-gray-300 mb-3">
|
||||||
Course Settings
|
Passing Percentage
|
||||||
</h1>
|
</label>
|
||||||
<p className="text-gray-400 mt-1">Configure general course properties and certificates for {course?.title}</p>
|
<div className="flex items-center gap-6">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={passingPercentage}
|
||||||
|
onChange={(e) => setPassingPercentage(parseInt(e.target.value))}
|
||||||
|
className="flex-1 h-2 bg-white/10 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
||||||
|
/>
|
||||||
|
<div className="text-4xl font-black text-blue-400 w-24 text-right">
|
||||||
|
{passingPercentage}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-3">
|
||||||
|
Students must achieve at least this percentage to pass the course.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Performance Tiers Preview */}
|
||||||
|
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||||
|
<h3 className="text-sm font-bold text-gray-300 mb-4">Performance Tiers Preview</h3>
|
||||||
|
<div className="space-y-3 text-xs">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-16 h-4 bg-red-500 rounded"></div>
|
||||||
|
<span className="text-red-400 font-bold">Reprobado:</span>
|
||||||
|
<span className="text-gray-400">0% - {Math.max(0, passingPercentage - 1)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-16 h-4 bg-orange-500 rounded"></div>
|
||||||
|
<span className="text-orange-400 font-bold">Rendimiento Bajo:</span>
|
||||||
|
<span className="text-gray-400">{passingPercentage}% - {passingPercentage + 9}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-16 h-4 bg-yellow-500 rounded"></div>
|
||||||
|
<span className="text-yellow-400 font-bold">Rendimiento Medio:</span>
|
||||||
|
<span className="text-gray-400">{passingPercentage + 10}% - {passingPercentage + 15}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-16 h-4 bg-green-500 rounded"></div>
|
||||||
|
<span className="text-green-400 font-bold">Buen Rendimiento:</span>
|
||||||
|
<span className="text-gray-400">{passingPercentage + 16}% - 90%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-16 h-4 bg-blue-500 rounded"></div>
|
||||||
|
<span className="text-blue-400 font-bold">Excelente:</span>
|
||||||
|
<span className="text-gray-400">91% - 100%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
</section>
|
||||||
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" : ""}`}
|
|
||||||
>
|
|
||||||
<Save size={18} />
|
|
||||||
{saving ? "Saving..." : "Save Changes"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CourseEditorLayout activeTab="settings">
|
{/* Course Pacing Section */}
|
||||||
<div className="space-y-8">
|
<section className="bg-white/5 border border-white/10 rounded-3xl p-8">
|
||||||
<TeamManagementSection courseId={id} />
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-green-500/10 flex items-center justify-center text-green-400">
|
||||||
{/* Passing Percentage Section */}
|
<Clock size={24} />
|
||||||
<section className="bg-white/5 border border-white/10 rounded-3xl p-8">
|
</div>
|
||||||
<div className="flex items-center gap-3 mb-6">
|
<h2 className="section-title">Ritmo y Calendario del Curso</h2>
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-bold text-gray-300 mb-3">
|
|
||||||
Passing Percentage
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center gap-6">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
value={passingPercentage}
|
|
||||||
onChange={(e) => setPassingPercentage(parseInt(e.target.value))}
|
|
||||||
className="flex-1 h-2 bg-white/10 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
|
||||||
/>
|
|
||||||
<div className="text-4xl font-black text-blue-400 w-24 text-right">
|
|
||||||
{passingPercentage}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-500 mt-3">
|
|
||||||
Students must achieve at least this percentage to pass the course.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Performance Tiers Preview */}
|
|
||||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
|
||||||
<h3 className="text-sm font-bold text-gray-300 mb-4">Performance Tiers Preview</h3>
|
|
||||||
<div className="space-y-3 text-xs">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-16 h-4 bg-red-500 rounded"></div>
|
|
||||||
<span className="text-red-400 font-bold">Reprobado:</span>
|
|
||||||
<span className="text-gray-400">0% - {Math.max(0, passingPercentage - 1)}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-16 h-4 bg-orange-500 rounded"></div>
|
|
||||||
<span className="text-orange-400 font-bold">Rendimiento Bajo:</span>
|
|
||||||
<span className="text-gray-400">{passingPercentage}% - {passingPercentage + 9}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-16 h-4 bg-yellow-500 rounded"></div>
|
|
||||||
<span className="text-yellow-400 font-bold">Rendimiento Medio:</span>
|
|
||||||
<span className="text-gray-400">{passingPercentage + 10}% - {passingPercentage + 15}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-16 h-4 bg-green-500 rounded"></div>
|
|
||||||
<span className="text-green-400 font-bold">Buen Rendimiento:</span>
|
|
||||||
<span className="text-gray-400">{passingPercentage + 16}% - 90%</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-16 h-4 bg-blue-500 rounded"></div>
|
|
||||||
<span className="text-blue-400 font-bold">Excelente:</span>
|
|
||||||
<span className="text-gray-400">91% - 100%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Course Pacing Section */}
|
|
||||||
<section className="bg-white/5 border border-white/10 rounded-3xl p-8">
|
|
||||||
<div className="flex items-center gap-3 mb-6">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<label className="block text-sm font-bold text-gray-300">Pacing Mode</label>
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<button
|
|
||||||
onClick={() => setPacingMode('self_paced')}
|
|
||||||
className={`flex-1 p-4 rounded-2xl border-2 transition-all text-left ${pacingMode === 'self_paced' ? 'border-blue-500 bg-blue-500/10' : 'border-white/5 bg-white/5 hover:border-white/10'}`}
|
|
||||||
>
|
|
||||||
<div className="font-bold">Self-Paced</div>
|
|
||||||
<div className="text-xs text-gray-500">Learners go at their own speed.</div>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setPacingMode('instructor_led')}
|
|
||||||
className={`flex-1 p-4 rounded-2xl border-2 transition-all text-left ${pacingMode === 'instructor_led' ? 'border-purple-500 bg-purple-500/10' : 'border-white/5 bg-white/5 hover:border-white/10'}`}
|
|
||||||
>
|
|
||||||
<div className="font-bold">Instructor-Led</div>
|
|
||||||
<div className="text-xs text-gray-500">Cohort-based with specific dates.</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{pacingMode === 'instructor_led' && (
|
|
||||||
<div className="space-y-4 animate-in fade-in slide-in-from-top-2">
|
|
||||||
<label className="block text-sm font-bold text-gray-300">Course Schedule</label>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-xs text-gray-500">Start Date</label>
|
|
||||||
<div className="relative">
|
|
||||||
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={startDate}
|
|
||||||
onChange={(e) => setStartDate(e.target.value)}
|
|
||||||
className="w-full bg-black/30 border border-white/10 rounded-xl py-2 pl-10 pr-4 text-sm focus:outline-none focus:border-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-xs text-gray-500">End Date</label>
|
|
||||||
<div className="relative">
|
|
||||||
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={endDate}
|
|
||||||
onChange={(e) => setEndDate(e.target.value)}
|
|
||||||
className="w-full bg-black/30 border border-white/10 rounded-xl py-2 pl-10 pr-4 text-sm focus:outline-none focus:border-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Course Pricing Section */}
|
|
||||||
<section className="bg-white/5 border border-white/10 rounded-3xl p-8">
|
|
||||||
<div className="flex items-center gap-3 mb-6">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<label className="block text-sm font-bold text-gray-300">Price</label>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
min="0"
|
|
||||||
value={price}
|
|
||||||
onChange={(e) => setPrice(parseFloat(e.target.value))}
|
|
||||||
className="w-full bg-black/30 border border-white/10 rounded-xl py-3 px-4 text-gray-900 dark:text-white focus:outline-none focus:border-blue-500 transition-colors"
|
|
||||||
placeholder="0.00"
|
|
||||||
/>
|
|
||||||
<div className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-500 font-bold">
|
|
||||||
{currency}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-500">Set to 0 for a free course.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<label className="block text-sm font-bold text-gray-300">Currency</label>
|
|
||||||
<select
|
|
||||||
value={currency}
|
|
||||||
onChange={(e) => setCurrency(e.target.value)}
|
|
||||||
className="w-full bg-black/30 border border-white/10 rounded-xl py-3 px-4 text-gray-900 dark:text-white focus:outline-none focus:border-blue-500 transition-colors appearance-none"
|
|
||||||
>
|
|
||||||
<option value="USD">USD - US Dollar</option>
|
|
||||||
<option value="CLP">CLP - Chilean Peso</option>
|
|
||||||
<option value="ARS">ARS - Argentine Peso</option>
|
|
||||||
<option value="BRL">BRL - Brazilian Real</option>
|
|
||||||
<option value="MXN">MXN - Mexican Peso</option>
|
|
||||||
<option value="COP">COP - Colombian Peso</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Certificate Template Section */}
|
|
||||||
<section className="bg-white/5 border border-white/10 rounded-3xl p-8">
|
|
||||||
<div className="flex items-center gap-3 mb-6">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<p className="text-gray-400">
|
|
||||||
Design the HTML certificate that students will receive upon passing the course.
|
|
||||||
Available variables: <code className="text-blue-400">{"{{student_name}}"}</code>, <code className="text-blue-400">{"{{course_title}}"}</code>, <code className="text-blue-400">{"{{date}}"}</code>, <code className="text-blue-400">{"{{score}}"}</code>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="block text-sm font-bold text-gray-300">HTML Template</label>
|
|
||||||
<textarea
|
|
||||||
value={certificateTemplate}
|
|
||||||
onChange={(e) => setCertificateTemplate(e.target.value)}
|
|
||||||
className="w-full h-[400px] bg-black/30 border border-white/10 rounded-xl p-4 font-mono text-sm text-gray-300 focus:outline-none focus:border-blue-500 transition-colors resize-none"
|
|
||||||
placeholder="Enter HTML code here..."
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => setCertificateTemplate(DEFAULT_CERTIFICATE_TEMPLATE)}
|
|
||||||
className="text-xs text-blue-400 hover:text-blue-300 underline"
|
|
||||||
>
|
|
||||||
Reset to Default Template
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="block text-sm font-bold text-gray-300">Live Preview</label>
|
|
||||||
<div className="w-full h-[400px] bg-white rounded-xl overflow-hidden relative group">
|
|
||||||
<iframe
|
|
||||||
srcDoc={certificateTemplate
|
|
||||||
.replace(/{{student_name}}/g, "Jane Doe")
|
|
||||||
.replace(/{{course_title}}/g, course?.title || "Demo Course")
|
|
||||||
.replace(/{{date}}/g, new Date().toLocaleDateString())
|
|
||||||
.replace(/{{score}}/g, "95")
|
|
||||||
}
|
|
||||||
className="w-full h-full transform scale-75 origin-top-left w-[133%] h-[133%]"
|
|
||||||
style={{ border: "none" }}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 pointer-events-none transition-colors" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{/* Course Portability Section */}
|
|
||||||
<section className="bg-white/5 border border-white/10 rounded-3xl p-8">
|
|
||||||
<div className="flex items-center gap-3 mb-6">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-sm font-bold text-gray-300">Export Course</h3>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
Download the entire course structure, modules, and lessons as a JSON file.
|
|
||||||
You can use this to backup or move content between organizations.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={handleExport}
|
|
||||||
disabled={exporting}
|
|
||||||
className="flex items-center gap-2 px-6 py-3 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl font-bold transition-all active:scale-95 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<Download size={18} />
|
|
||||||
{exporting ? "Exporting..." : "Download JSON"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-sm font-bold text-gray-300">Import Course</h3>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
Upload a previously exported course JSON file. This will create a NEW course
|
|
||||||
within the current organization based on that data.
|
|
||||||
</p>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept=".json"
|
|
||||||
onChange={handleImport}
|
|
||||||
disabled={importing}
|
|
||||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
disabled={importing}
|
|
||||||
className="flex items-center gap-2 px-6 py-3 bg-indigo-600/20 hover:bg-indigo-600/30 border border-indigo-500/30 text-indigo-400 rounded-xl font-bold transition-all pointer-events-none"
|
|
||||||
>
|
|
||||||
<Upload size={18} />
|
|
||||||
{importing ? "Importing..." : "Upload JSON"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</CourseEditorLayout>
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<label className="block text-sm font-bold text-gray-300">Pacing Mode</label>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setPacingMode('self_paced')}
|
||||||
|
className={`flex-1 p-4 rounded-2xl border-2 transition-all text-left ${pacingMode === 'self_paced' ? 'border-blue-500 bg-blue-500/10' : 'border-white/5 bg-white/5 hover:border-white/10'}`}
|
||||||
|
>
|
||||||
|
<div className="font-bold">Self-Paced</div>
|
||||||
|
<div className="text-xs text-gray-500">Learners go at their own speed.</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setPacingMode('instructor_led')}
|
||||||
|
className={`flex-1 p-4 rounded-2xl border-2 transition-all text-left ${pacingMode === 'instructor_led' ? 'border-purple-500 bg-purple-500/10' : 'border-white/5 bg-white/5 hover:border-white/10'}`}
|
||||||
|
>
|
||||||
|
<div className="font-bold">Instructor-Led</div>
|
||||||
|
<div className="text-xs text-gray-500">Cohort-based with specific dates.</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pacingMode === 'instructor_led' && (
|
||||||
|
<div className="space-y-4 animate-in fade-in slide-in-from-top-2">
|
||||||
|
<label className="block text-sm font-bold text-gray-300">Course Schedule</label>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs text-gray-500">Start Date</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={startDate}
|
||||||
|
onChange={(e) => setStartDate(e.target.value)}
|
||||||
|
className="w-full bg-black/30 border border-white/10 rounded-xl py-2 pl-10 pr-4 text-sm focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs text-gray-500">End Date</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={endDate}
|
||||||
|
onChange={(e) => setEndDate(e.target.value)}
|
||||||
|
className="w-full bg-black/30 border border-white/10 rounded-xl py-2 pl-10 pr-4 text-sm focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Course Pricing Section */}
|
||||||
|
<section className="bg-white/5 border border-white/10 rounded-3xl p-8">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<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="section-title">Precio del Curso</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<label className="block text-sm font-bold text-gray-300">Price</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
value={price}
|
||||||
|
onChange={(e) => setPrice(parseFloat(e.target.value))}
|
||||||
|
className="w-full bg-black/30 border border-white/10 rounded-xl py-3 px-4 text-gray-900 dark:text-white focus:outline-none focus:border-blue-500 transition-colors"
|
||||||
|
placeholder="0.00"
|
||||||
|
/>
|
||||||
|
<div className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-500 font-bold">
|
||||||
|
{currency}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500">Set to 0 for a free course.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<label className="block text-sm font-bold text-gray-300">Currency</label>
|
||||||
|
<select
|
||||||
|
value={currency}
|
||||||
|
onChange={(e) => setCurrency(e.target.value)}
|
||||||
|
className="w-full bg-black/30 border border-white/10 rounded-xl py-3 px-4 text-gray-900 dark:text-white focus:outline-none focus:border-blue-500 transition-colors appearance-none"
|
||||||
|
>
|
||||||
|
<option value="USD">USD - US Dollar</option>
|
||||||
|
<option value="CLP">CLP - Chilean Peso</option>
|
||||||
|
<option value="ARS">ARS - Argentine Peso</option>
|
||||||
|
<option value="BRL">BRL - Brazilian Real</option>
|
||||||
|
<option value="MXN">MXN - Mexican Peso</option>
|
||||||
|
<option value="COP">COP - Colombian Peso</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Certificate Template Section */}
|
||||||
|
<section className="bg-white/5 border border-white/10 rounded-3xl p-8">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<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="section-title">Plantilla de Certificado</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<p className="text-gray-400">
|
||||||
|
Design the HTML certificate that students will receive upon passing the course.
|
||||||
|
Available variables: <code className="text-blue-400">{"{{student_name}}"}</code>, <code className="text-blue-400">{"{{course_title}}"}</code>, <code className="text-blue-400">{"{{date}}"}</code>, <code className="text-blue-400">{"{{score}}"}</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-bold text-gray-300">HTML Template</label>
|
||||||
|
<textarea
|
||||||
|
value={certificateTemplate}
|
||||||
|
onChange={(e) => setCertificateTemplate(e.target.value)}
|
||||||
|
className="w-full h-[400px] bg-black/30 border border-white/10 rounded-xl p-4 font-mono text-sm text-gray-300 focus:outline-none focus:border-blue-500 transition-colors resize-none"
|
||||||
|
placeholder="Enter HTML code here..."
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => setCertificateTemplate(DEFAULT_CERTIFICATE_TEMPLATE)}
|
||||||
|
className="text-xs text-blue-400 hover:text-blue-300 underline"
|
||||||
|
>
|
||||||
|
Reset to Default Template
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block text-sm font-bold text-gray-300">Live Preview</label>
|
||||||
|
<div className="w-full h-[400px] bg-white rounded-xl overflow-hidden relative group">
|
||||||
|
<iframe
|
||||||
|
srcDoc={certificateTemplate
|
||||||
|
.replace(/{{student_name}}/g, "Jane Doe")
|
||||||
|
.replace(/{{course_title}}/g, course?.title || "Demo Course")
|
||||||
|
.replace(/{{date}}/g, new Date().toLocaleDateString())
|
||||||
|
.replace(/{{score}}/g, "95")
|
||||||
|
}
|
||||||
|
className="w-full h-full transform scale-75 origin-top-left w-[133%] h-[133%]"
|
||||||
|
style={{ border: "none" }}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 pointer-events-none transition-colors" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/* Course Portability Section */}
|
||||||
|
<section className="bg-white/5 border border-white/10 rounded-3xl p-8">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<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="section-title">Portabilidad del Curso</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-bold text-gray-300">Export Course</h3>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Download the entire course structure, modules, and lessons as a JSON file.
|
||||||
|
You can use this to backup or move content between organizations.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={handleExport}
|
||||||
|
disabled={exporting}
|
||||||
|
className="flex items-center gap-2 px-6 py-3 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl font-bold transition-all active:scale-95 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Download size={18} />
|
||||||
|
{exporting ? "Exporting..." : "Download JSON"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-bold text-gray-300">Import Course</h3>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Upload a previously exported course JSON file. This will create a NEW course
|
||||||
|
within the current organization based on that data.
|
||||||
|
</p>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
onChange={handleImport}
|
||||||
|
disabled={importing}
|
||||||
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
disabled={importing}
|
||||||
|
className="flex items-center gap-2 px-6 py-3 bg-indigo-600/20 hover:bg-indigo-600/30 border border-indigo-500/30 text-indigo-400 rounded-xl font-bold transition-all pointer-events-none"
|
||||||
|
>
|
||||||
|
<Upload size={18} />
|
||||||
|
{importing ? "Importing..." : "Upload JSON"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CourseEditorLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,185 +116,180 @@ export default function CourseStudentsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-transparent text-gray-900 dark:text-white p-8">
|
<>
|
||||||
<div className="max-w-7xl mx-auto">
|
<CourseEditorLayout
|
||||||
<div className="flex items-center justify-between mb-8">
|
activeTab="students"
|
||||||
<div className="flex items-center gap-4">
|
pageTitle="Estudiantes y Grupos"
|
||||||
<button onClick={() => router.back()} className="p-2 hover:bg-white/10 rounded-full transition-colors">
|
pageDescription="Gestiona las inscripciones y segmenta a tu audiencia por cohortes."
|
||||||
<ArrowLeft className="w-6 h-6" />
|
pageActions={
|
||||||
</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>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsEnrollModalOpen(true)}
|
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} />
|
<UserPlus size={18} />
|
||||||
Enroll Students
|
Inscribir Estudiantes
|
||||||
</button>
|
</button>
|
||||||
</div>
|
}
|
||||||
|
>
|
||||||
<CourseEditorLayout activeTab="students">
|
<div className="space-y-8">
|
||||||
<div className="space-y-6">
|
<h2 className="section-title">
|
||||||
{/* Search and Filters */}
|
<Users className="text-blue-500" />
|
||||||
<div className="glass p-4 rounded-2xl flex flex-col md:flex-row items-center gap-4">
|
Listado de Estudiantes
|
||||||
<div className="relative flex-1 w-full">
|
</h2>
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 w-4 h-4" />
|
{/* Search and Filters */}
|
||||||
<input
|
<div className="glass p-4 rounded-2xl flex flex-col md:flex-row items-center gap-4">
|
||||||
type="text"
|
<div className="relative flex-1 w-full">
|
||||||
placeholder="Search by name or email..."
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 w-4 h-4" />
|
||||||
value={searchTerm}
|
<input
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
type="text"
|
||||||
className="w-full bg-black/20 border border-white/10 rounded-xl py-2 pl-10 pr-4 text-sm focus:outline-none focus:border-blue-500/50 transition-all font-medium"
|
placeholder="Search by name or email..."
|
||||||
/>
|
value={searchTerm}
|
||||||
</div>
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
<div className="flex items-center gap-2 w-full md:w-auto">
|
className="w-full bg-black/20 border border-white/10 rounded-xl py-2 pl-10 pr-4 text-sm focus:outline-none focus:border-blue-500/50 transition-all font-medium"
|
||||||
<Filter size={16} className="text-gray-400" />
|
/>
|
||||||
<select
|
|
||||||
className="bg-black/20 border border-white/10 rounded-xl px-4 py-2 text-sm focus:outline-none focus:border-blue-500/50 text-gray-900 dark:text-white min-w-[150px]"
|
|
||||||
value={selectedCohortId}
|
|
||||||
onChange={(e) => setSelectedCohortId(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="all">All Cohorts</option>
|
|
||||||
{cohorts.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2 w-full md:w-auto">
|
||||||
{/* Student List */}
|
<Filter size={16} className="text-gray-400" />
|
||||||
<div className="rounded-3xl border border-white/10 bg-white/[0.02] overflow-hidden">
|
<select
|
||||||
<table className="w-full text-left border-collapse">
|
className="bg-black/20 border border-white/10 rounded-xl px-4 py-2 text-sm focus:outline-none focus:border-blue-500/50 text-gray-900 dark:text-white min-w-[150px]"
|
||||||
<thead>
|
value={selectedCohortId}
|
||||||
<tr className="bg-white/5 border-b border-white/5 font-bold text-xs uppercase tracking-widest text-gray-500 uppercase">
|
onChange={(e) => setSelectedCohortId(e.target.value)}
|
||||||
<th className="p-6">Student</th>
|
>
|
||||||
<th className="p-6">Enrollment Date</th>
|
<option value="all">All Cohorts</option>
|
||||||
<th className="p-6 text-right">Actions</th>
|
{cohorts.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||||
</tr>
|
</select>
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-white/5">
|
|
||||||
{filteredStudents.length === 0 ? (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={3} className="p-12 text-center text-gray-500 italic">No students found.</td>
|
|
||||||
</tr>
|
|
||||||
) : filteredStudents.map(student => (
|
|
||||||
<tr key={student.user_id} className="hover:bg-white/[0.02] transition-colors group">
|
|
||||||
<td className="p-6">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center font-bold text-sm">
|
|
||||||
{student.full_name.charAt(0)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-bold text-gray-900 dark:text-white group-hover:text-blue-400 transition-colors uppercase tracking-tight">{student.full_name}</div>
|
|
||||||
<div className="text-xs text-gray-500 flex items-center gap-1 mt-0.5"><Mail size={12} /> {student.email}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="p-6 text-gray-400 text-sm font-medium">
|
|
||||||
{/* In a real app we'd have the enrollment date here */}
|
|
||||||
Available upon request
|
|
||||||
</td>
|
|
||||||
<td className="p-6 text-right">
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
|
||||||
<div className="relative group/actions">
|
|
||||||
<button className="p-2 hover:bg-white/10 rounded-lg transition-colors text-gray-500">
|
|
||||||
<MoreHorizontal size={20} />
|
|
||||||
</button>
|
|
||||||
<div className="absolute right-0 top-full mt-2 w-48 bg-[#1a1c1e] border border-white/10 rounded-xl shadow-2xl invisible group-hover/actions:visible z-10 p-2">
|
|
||||||
<div className="text-[10px] font-black uppercase tracking-widest text-gray-600 px-3 py-2 border-b border-white/5 mb-1">Move to Cohort</div>
|
|
||||||
{cohorts.map(c => (
|
|
||||||
<button
|
|
||||||
key={c.id}
|
|
||||||
onClick={() => handleCohortAssignment(student.user_id, c.id)}
|
|
||||||
className="w-full text-left px-3 py-2 text-xs hover:bg-white/5 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
{c.name}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
<button
|
|
||||||
className="w-full text-left px-3 py-2 text-xs text-red-400 hover:bg-red-500/10 rounded-lg transition-colors mt-2 border-t border-white/5"
|
|
||||||
onClick={() => { if (confirm("Unenroll student?")) handleCohortAssignment(student.user_id, "", true) }}
|
|
||||||
>
|
|
||||||
Unenroll
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CourseEditorLayout>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Enroll Modal */}
|
{/* Student List */}
|
||||||
{isEnrollModalOpen && (
|
<div className="rounded-3xl border border-white/10 bg-white/[0.02] overflow-hidden">
|
||||||
<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">
|
<table className="w-full text-left border-collapse">
|
||||||
<div className="bg-[#16181b] border border-white/10 rounded-3xl w-full max-w-2xl overflow-hidden shadow-2xl scale-in-center">
|
<thead>
|
||||||
<div className="p-6 border-b border-white/5 flex items-center justify-between">
|
<tr className="bg-white/5 border-b border-white/5 font-bold text-xs uppercase tracking-widest text-gray-500 uppercase">
|
||||||
<h2 className="text-xl font-bold flex items-center gap-2">
|
<th className="p-6">Student</th>
|
||||||
<UserPlus className="text-blue-500" />
|
<th className="p-6">Enrollment Date</th>
|
||||||
Enroll Organization Students
|
<th className="p-6 text-right">Actions</th>
|
||||||
</h2>
|
</tr>
|
||||||
<button onClick={() => setIsEnrollModalOpen(false)} className="p-2 hover:bg-white/10 rounded-full transition-colors">
|
</thead>
|
||||||
<X size={20} />
|
<tbody className="divide-y divide-white/5">
|
||||||
</button>
|
{filteredStudents.length === 0 ? (
|
||||||
</div>
|
<tr>
|
||||||
<div className="p-8 space-y-6">
|
<td colSpan={3} className="p-12 text-center text-gray-500 italic">No students found.</td>
|
||||||
<div className="relative">
|
</tr>
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 w-4 h-4" />
|
) : filteredStudents.map(student => (
|
||||||
<input
|
<tr key={student.user_id} className="hover:bg-white/[0.02] transition-colors group">
|
||||||
type="text"
|
<td className="p-6">
|
||||||
placeholder="Search and select students from organization..."
|
<div className="flex items-center gap-4">
|
||||||
value={enrollSearch}
|
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center font-bold text-sm">
|
||||||
onChange={(e) => setEnrollSearch(e.target.value)}
|
{student.full_name.charAt(0)}
|
||||||
className="w-full bg-black/20 border border-white/10 rounded-xl py-3 pl-10 pr-4 text-sm focus:outline-none"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2 max-h-[300px] overflow-y-auto pr-2 custom-scrollbar">
|
|
||||||
{orgUsersLoading ? (
|
|
||||||
<div className="flex justify-center p-8"><Loader2 className="w-8 h-8 animate-spin text-blue-500" /></div>
|
|
||||||
) : allOrgUsers.filter(u => u.full_name.toLowerCase().includes(enrollSearch.toLowerCase())).length === 0 ? (
|
|
||||||
<div className="text-center p-8 text-gray-500 italic">No remaining students to enroll.</div>
|
|
||||||
) : (
|
|
||||||
allOrgUsers.filter(u => u.full_name.toLowerCase().includes(enrollSearch.toLowerCase())).map(user => (
|
|
||||||
<div key={user.id} className="flex items-center justify-between p-4 bg-white/5 border border-white/10 rounded-xl">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<UserCircle className="text-gray-500" />
|
|
||||||
<div>
|
<div>
|
||||||
<div className="font-bold text-sm tracking-tight">{user.full_name}</div>
|
<div className="font-bold text-gray-900 dark:text-white group-hover:text-blue-400 transition-colors uppercase tracking-tight">{student.full_name}</div>
|
||||||
<div className="text-xs text-gray-500">{user.email}</div>
|
<div className="text-xs text-gray-500 flex items-center gap-1 mt-0.5"><Mail size={12} /> {student.email}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
</td>
|
||||||
onClick={() => handleEnroll([user.email])}
|
<td className="p-6 text-gray-400 text-sm font-medium">
|
||||||
className="px-4 py-1.5 bg-blue-600 hover:bg-blue-500 text-gray-900 dark:text-white text-xs font-bold rounded-lg transition-all"
|
{/* In a real app we'd have the enrollment date here */}
|
||||||
>
|
Available upon request
|
||||||
Enroll Now
|
</td>
|
||||||
</button>
|
<td className="p-6 text-right">
|
||||||
</div>
|
<div className="flex items-center justify-end gap-2">
|
||||||
))
|
<div className="relative group/actions">
|
||||||
)}
|
<button className="p-2 hover:bg-white/10 rounded-lg transition-colors text-gray-500">
|
||||||
</div>
|
<MoreHorizontal size={20} />
|
||||||
|
</button>
|
||||||
|
<div className="absolute right-0 top-full mt-2 w-48 bg-[#1a1c1e] border border-white/10 rounded-xl shadow-2xl invisible group-hover/actions:visible z-10 p-2">
|
||||||
|
<div className="text-[10px] font-black uppercase tracking-widest text-gray-600 px-3 py-2 border-b border-white/5 mb-1">Move to Cohort</div>
|
||||||
|
{cohorts.map(c => (
|
||||||
|
<button
|
||||||
|
key={c.id}
|
||||||
|
onClick={() => handleCohortAssignment(student.user_id, c.id)}
|
||||||
|
className="w-full text-left px-3 py-2 text-xs hover:bg-white/5 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
{c.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
className="w-full text-left px-3 py-2 text-xs text-red-400 hover:bg-red-500/10 rounded-lg transition-colors mt-2 border-t border-white/5"
|
||||||
|
onClick={() => { if (confirm("Unenroll student?")) handleCohortAssignment(student.user_id, "", true) }}
|
||||||
|
>
|
||||||
|
Unenroll
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CourseEditorLayout>
|
||||||
|
|
||||||
<div className="bg-blue-500/10 border border-blue-500/20 p-4 rounded-xl flex gap-3">
|
{/* Enroll Modal */}
|
||||||
<Plus size={20} className="text-blue-400 shrink-0" />
|
{
|
||||||
<div className="text-xs text-blue-300 leading-relaxed font-medium">
|
isEnrollModalOpen && (
|
||||||
You can also enroll external students by going to the <strong>Gradebook</strong> and using the Bulk Enroll feature.
|
<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">
|
||||||
|
<h2 className="text-xl font-bold flex items-center gap-2">
|
||||||
|
<UserPlus className="text-blue-500" />
|
||||||
|
Enroll Organization Students
|
||||||
|
</h2>
|
||||||
|
<button onClick={() => setIsEnrollModalOpen(false)} className="p-2 hover:bg-white/10 rounded-full transition-colors">
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-8 space-y-6">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 w-4 h-4" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search and select students from organization..."
|
||||||
|
value={enrollSearch}
|
||||||
|
onChange={(e) => setEnrollSearch(e.target.value)}
|
||||||
|
className="w-full bg-black/20 border border-white/10 rounded-xl py-3 pl-10 pr-4 text-sm focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 max-h-[300px] overflow-y-auto pr-2 custom-scrollbar">
|
||||||
|
{orgUsersLoading ? (
|
||||||
|
<div className="flex justify-center p-8"><Loader2 className="w-8 h-8 animate-spin text-blue-500" /></div>
|
||||||
|
) : allOrgUsers.filter(u => u.full_name.toLowerCase().includes(enrollSearch.toLowerCase())).length === 0 ? (
|
||||||
|
<div className="text-center p-8 text-gray-500 italic">No remaining students to enroll.</div>
|
||||||
|
) : (
|
||||||
|
allOrgUsers.filter(u => u.full_name.toLowerCase().includes(enrollSearch.toLowerCase())).map(user => (
|
||||||
|
<div key={user.id} className="flex items-center justify-between p-4 bg-white/5 border border-white/10 rounded-xl">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<UserCircle className="text-gray-500" />
|
||||||
|
<div>
|
||||||
|
<div className="font-bold text-sm tracking-tight">{user.full_name}</div>
|
||||||
|
<div className="text-xs text-gray-500">{user.email}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleEnroll([user.email])}
|
||||||
|
className="px-4 py-1.5 bg-blue-600 hover:bg-blue-500 text-gray-900 dark:text-white text-xs font-bold rounded-lg transition-all"
|
||||||
|
>
|
||||||
|
Enroll Now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-blue-500/10 border border-blue-500/20 p-4 rounded-xl flex gap-3">
|
||||||
|
<Plus size={20} className="text-blue-400 shrink-0" />
|
||||||
|
<div className="text-xs text-blue-300 leading-relaxed font-medium">
|
||||||
|
You can also enroll external students by going to the <strong>Gradebook</strong> and using the Bulk Enroll feature.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,162 +96,168 @@ export default function CourseTeamPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto p-8">
|
<>
|
||||||
<div className="flex items-center justify-between mb-8">
|
<CourseEditorLayout
|
||||||
<div>
|
activeTab="team"
|
||||||
<h1 className="text-3xl font-bold">Course Team</h1>
|
pageTitle="Equipo del Curso"
|
||||||
<p className="text-gray-400 mt-1">Manage multiple instructors and assistants for this course</p>
|
pageDescription="Gestiona múltiples instructores y asistentes para este curso."
|
||||||
</div>
|
pageActions={
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsAddModalOpen(true)}
|
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>
|
</button>
|
||||||
</div>
|
}
|
||||||
|
>
|
||||||
<CourseEditorLayout activeTab="team">
|
<div className="space-y-8">
|
||||||
<div className="grid gap-4">
|
<h2 className="section-title">
|
||||||
{loading ? (
|
<ShieldCheck className="text-blue-500" />
|
||||||
<div className="py-20 text-center text-gray-500 animate-pulse">Loading team members...</div>
|
Miembros del Equipo
|
||||||
) : instructors.length === 0 ? (
|
</h2>
|
||||||
<div className="py-20 glass rounded-2xl flex flex-col items-center justify-center text-gray-500 gap-4">
|
<div className="grid gap-4">
|
||||||
<GraduationCap className="w-12 h-12 opacity-20" />
|
{loading ? (
|
||||||
<p>No instructors assigned yet.</p>
|
<div className="py-20 text-center text-gray-500 animate-pulse">Loading team members...</div>
|
||||||
</div>
|
) : instructors.length === 0 ? (
|
||||||
) : (
|
<div className="py-20 glass rounded-2xl flex flex-col items-center justify-center text-gray-500 gap-4">
|
||||||
instructors.map((inst) => (
|
<GraduationCap className="w-12 h-12 opacity-20" />
|
||||||
<div key={inst.id} className="glass p-6 rounded-2xl border-white/5 flex items-center justify-between group">
|
<p>No instructors assigned yet.</p>
|
||||||
<div className="flex items-center gap-6">
|
</div>
|
||||||
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-xl font-bold border-2 border-white/10 shadow-xl">
|
) : (
|
||||||
{inst.full_name?.charAt(0) || inst.email?.charAt(0)}
|
instructors.map((inst) => (
|
||||||
</div>
|
<div key={inst.id} className="glass p-6 rounded-2xl border-white/5 flex items-center justify-between group">
|
||||||
<div className="space-y-1">
|
<div className="flex items-center gap-6">
|
||||||
<h3 className="text-lg font-bold flex items-center gap-2">
|
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-xl font-bold border-2 border-white/10 shadow-xl">
|
||||||
{inst.full_name}
|
{inst.full_name?.charAt(0) || inst.email?.charAt(0)}
|
||||||
{inst.role === 'primary' && (
|
</div>
|
||||||
<span className="text-[10px] font-black uppercase px-2 py-0.5 bg-orange-500/10 text-orange-400 border border-orange-500/20 rounded-full">
|
<div className="space-y-1">
|
||||||
Owner
|
<h3 className="text-lg font-bold flex items-center gap-2">
|
||||||
|
{inst.full_name}
|
||||||
|
{inst.role === 'primary' && (
|
||||||
|
<span className="text-[10px] font-black uppercase px-2 py-0.5 bg-orange-500/10 text-orange-400 border border-orange-500/20 rounded-full">
|
||||||
|
Owner
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-3 text-sm text-gray-400">
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<Mail className="w-3.5 h-3.5 opacity-60" /> {inst.email}
|
||||||
</span>
|
</span>
|
||||||
)}
|
<span className="w-1 h-1 rounded-full bg-gray-700" />
|
||||||
</h3>
|
<span className="flex items-center gap-1.5">
|
||||||
<div className="flex items-center gap-3 text-sm text-gray-400">
|
{getRoleIcon(inst.role)} {getRoleLabel(inst.role)}
|
||||||
<span className="flex items-center gap-1.5">
|
</span>
|
||||||
<Mail className="w-3.5 h-3.5 opacity-60" /> {inst.email}
|
</div>
|
||||||
</span>
|
|
||||||
<span className="w-1 h-1 rounded-full bg-gray-700" />
|
|
||||||
<span className="flex items-center gap-1.5">
|
|
||||||
{getRoleIcon(inst.role)} {getRoleLabel(inst.role)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{inst.role !== 'primary' && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemoveMember(inst.user_id)}
|
||||||
|
className="p-3 rounded-xl hover:bg-red-500/10 text-gray-500 hover:text-red-400 transition-all active:scale-95"
|
||||||
|
title="Remove member"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
))
|
||||||
{inst.role !== 'primary' && (
|
)}
|
||||||
<button
|
</div>
|
||||||
onClick={() => handleRemoveMember(inst.user_id)}
|
|
||||||
className="p-3 rounded-xl hover:bg-red-500/10 text-gray-500 hover:text-red-400 transition-all active:scale-95"
|
|
||||||
title="Remove member"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</CourseEditorLayout>
|
</CourseEditorLayout>
|
||||||
|
|
||||||
{/* Add Member Modal */}
|
{/* Add Member Modal */}
|
||||||
{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">
|
isAddModalOpen && (
|
||||||
<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="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="p-8 border-b border-white/5 flex justify-between items-center">
|
<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>
|
<div className="p-8 border-b border-white/5 flex justify-between items-center">
|
||||||
<h2 className="text-2xl font-bold">Add Team Member</h2>
|
<div>
|
||||||
<p className="text-sm text-gray-400 mt-1">Search for a user by name or email</p>
|
<h2 className="text-2xl font-bold">Add Team Member</h2>
|
||||||
</div>
|
<p className="text-sm text-gray-400 mt-1">Search for a user by name or email</p>
|
||||||
<button onClick={() => setIsAddModalOpen(false)} className="p-2 hover:bg-white/5 rounded-full transition-colors text-gray-500">
|
</div>
|
||||||
<X className="w-6 h-6" />
|
<button onClick={() => setIsAddModalOpen(false)} className="p-2 hover:bg-white/5 rounded-full transition-colors text-gray-500">
|
||||||
</button>
|
<X className="w-6 h-6" />
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
<div className="p-8 space-y-8">
|
|
||||||
<form onSubmit={handleSearch} className="relative">
|
|
||||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
|
|
||||||
<input
|
|
||||||
autoFocus
|
|
||||||
placeholder="Type name or email..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="w-full bg-white/5 border border-white/10 rounded-2xl pl-12 pr-4 py-4 text-sm focus:outline-none focus:border-blue-500 transition-all font-medium"
|
|
||||||
/>
|
|
||||||
{searching && (
|
|
||||||
<div className="absolute right-4 top-1/2 -translate-y-1/2">
|
|
||||||
<div className="w-5 h-5 border-2 border-white/20 border-t-blue-500 rounded-full animate-spin" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<label className="block">
|
|
||||||
<span className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-3 block">Assign Role</span>
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<button
|
|
||||||
onClick={() => setSelectedRole('instructor')}
|
|
||||||
className={`flex items-center gap-3 p-4 rounded-2xl border transition-all text-left ${selectedRole === 'instructor' ? 'bg-blue-600/10 border-blue-500/50 text-blue-400' : 'bg-white/5 border-white/10 text-gray-500 hover:border-white/20'}`}
|
|
||||||
>
|
|
||||||
<ShieldCheck className="w-5 h-5" />
|
|
||||||
<div>
|
|
||||||
<div className="font-bold text-sm">Instructor</div>
|
|
||||||
<div className="text-[10px] mt-0.5 opacity-60">Full access</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setSelectedRole('assistant')}
|
|
||||||
className={`flex items-center gap-3 p-4 rounded-2xl border transition-all text-left ${selectedRole === 'assistant' ? 'bg-blue-600/10 border-blue-500/50 text-blue-400' : 'bg-white/5 border-white/10 text-gray-500 hover:border-white/20'}`}
|
|
||||||
>
|
|
||||||
<Shield className="w-5 h-5" />
|
|
||||||
<div>
|
|
||||||
<div className="font-bold text-sm">Assistant</div>
|
|
||||||
<div className="text-[10px] mt-0.5 opacity-60">Limited access</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3 max-h-60 overflow-y-auto pr-2 custom-scrollbar">
|
<div className="p-8 space-y-8">
|
||||||
{users.length > 0 ? (
|
<form onSubmit={handleSearch} className="relative">
|
||||||
users.map(u => (
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
|
||||||
<button
|
<input
|
||||||
key={u.id}
|
autoFocus
|
||||||
onClick={() => handleAddMember(u)}
|
placeholder="Type name or email..."
|
||||||
className="w-full flex items-center justify-between p-4 rounded-2xl bg-white/5 border border-white/10 hover:border-blue-500/30 hover:bg-white/10 transition-all group"
|
value={searchQuery}
|
||||||
>
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
<div className="flex items-center gap-4 text-left">
|
className="w-full bg-white/5 border border-white/10 rounded-2xl pl-12 pr-4 py-4 text-sm focus:outline-none focus:border-blue-500 transition-all font-medium"
|
||||||
<div className="w-10 h-10 rounded-full bg-white/10 flex items-center justify-center font-bold text-sm">
|
/>
|
||||||
{u.full_name.charAt(0)}
|
{searching && (
|
||||||
</div>
|
<div className="absolute right-4 top-1/2 -translate-y-1/2">
|
||||||
|
<div className="w-5 h-5 border-2 border-white/20 border-t-blue-500 rounded-full animate-spin" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<label className="block">
|
||||||
|
<span className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-3 block">Assign Role</span>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedRole('instructor')}
|
||||||
|
className={`flex items-center gap-3 p-4 rounded-2xl border transition-all text-left ${selectedRole === 'instructor' ? 'bg-blue-600/10 border-blue-500/50 text-blue-400' : 'bg-white/5 border-white/10 text-gray-500 hover:border-white/20'}`}
|
||||||
|
>
|
||||||
|
<ShieldCheck className="w-5 h-5" />
|
||||||
<div>
|
<div>
|
||||||
<div className="font-bold text-sm">{u.full_name}</div>
|
<div className="font-bold text-sm">Instructor</div>
|
||||||
<div className="text-xs text-gray-500">{u.email}</div>
|
<div className="text-[10px] mt-0.5 opacity-60">Full access</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
<div className="w-8 h-8 rounded-lg bg-blue-500/0 flex items-center justify-center group-hover:bg-blue-500/20 transition-all">
|
<button
|
||||||
<Plus className="w-4 h-4 text-blue-400 opacity-0 group-hover:opacity-100" />
|
onClick={() => setSelectedRole('assistant')}
|
||||||
</div>
|
className={`flex items-center gap-3 p-4 rounded-2xl border transition-all text-left ${selectedRole === 'assistant' ? 'bg-blue-600/10 border-blue-500/50 text-blue-400' : 'bg-white/5 border-white/10 text-gray-500 hover:border-white/20'}`}
|
||||||
</button>
|
>
|
||||||
))
|
<Shield className="w-5 h-5" />
|
||||||
) : searchQuery && !searching ? (
|
<div>
|
||||||
<div className="py-8 text-center text-gray-600 italic text-sm">No users found matching your search.</div>
|
<div className="font-bold text-sm">Assistant</div>
|
||||||
) : null}
|
<div className="text-[10px] mt-0.5 opacity-60">Limited access</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 max-h-60 overflow-y-auto pr-2 custom-scrollbar">
|
||||||
|
{users.length > 0 ? (
|
||||||
|
users.map(u => (
|
||||||
|
<button
|
||||||
|
key={u.id}
|
||||||
|
onClick={() => handleAddMember(u)}
|
||||||
|
className="w-full flex items-center justify-between p-4 rounded-2xl bg-white/5 border border-white/10 hover:border-blue-500/30 hover:bg-white/10 transition-all group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4 text-left">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-white/10 flex items-center justify-center font-bold text-sm">
|
||||||
|
{u.full_name.charAt(0)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-bold text-sm">{u.full_name}</div>
|
||||||
|
<div className="text-xs text-gray-500">{u.email}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-blue-500/0 flex items-center justify-center group-hover:bg-blue-500/20 transition-all">
|
||||||
|
<Plus className="w-4 h-4 text-blue-400 opacity-0 group-hover:opacity-100" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : searchQuery && !searching ? (
|
||||||
|
<div className="py-8 text-center text-gray-600 italic text-sm">No users found matching your search.</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,30 @@ body {
|
|||||||
@apply scale-95;
|
@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 */
|
/* Custom Scrollbar */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
|
|||||||
@@ -141,15 +141,15 @@ export default function CourseEditorLayout({
|
|||||||
<ChevronLeft className="w-5 h-5" />
|
<ChevronLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-black text-gray-900 dark:text-white tracking-tight leading-tight">
|
<h1 className="heading-premium">
|
||||||
{displayTitle}
|
{displayTitle}
|
||||||
</h1>
|
</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}
|
{displayDescription}
|
||||||
{course?.pacing_mode && (
|
{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-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"
|
: "bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-400"
|
||||||
}`}>
|
}`}>
|
||||||
{course.pacing_mode.replace("_", " ").toUpperCase()}
|
{course.pacing_mode.replace("_", " ").toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
@@ -179,8 +179,8 @@ export default function CourseEditorLayout({
|
|||||||
<Link
|
<Link
|
||||||
href={groupHref}
|
href={groupHref}
|
||||||
className={`flex items-center gap-2 px-6 py-3 text-sm font-black uppercase tracking-wider transition-all border-b-2 whitespace-nowrap ${isGroupActive
|
className={`flex items-center gap-2 px-6 py-3 text-sm font-black uppercase tracking-wider transition-all border-b-2 whitespace-nowrap ${isGroupActive
|
||||||
? "border-blue-600 dark:border-blue-400 text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/10"
|
? "border-blue-600 dark:border-blue-400 text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/10"
|
||||||
: "border-transparent text-slate-500 dark:text-gray-400 hover:text-slate-800 dark:hover:text-gray-100 hover:bg-slate-50 dark:hover:bg-white/5"
|
: "border-transparent text-slate-500 dark:text-gray-400 hover:text-slate-800 dark:hover:text-gray-100 hover:bg-slate-50 dark:hover:bg-white/5"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className="w-4 h-4 flex-shrink-0" aria-hidden="true" />
|
<Icon className="w-4 h-4 flex-shrink-0" aria-hidden="true" />
|
||||||
@@ -206,8 +206,8 @@ export default function CourseEditorLayout({
|
|||||||
href={tab.href}
|
href={tab.href}
|
||||||
aria-current={isActive ? "page" : undefined}
|
aria-current={isActive ? "page" : undefined}
|
||||||
className={`flex items-center gap-1.5 px-4 py-2 my-1 text-xs font-bold uppercase tracking-wider rounded-lg transition-all whitespace-nowrap ${isActive
|
className={`flex items-center gap-1.5 px-4 py-2 my-1 text-xs font-bold uppercase tracking-wider rounded-lg transition-all whitespace-nowrap ${isActive
|
||||||
? "bg-blue-600 text-white shadow-sm shadow-blue-500/30"
|
? "bg-blue-600 text-white shadow-sm shadow-blue-500/30"
|
||||||
: "text-slate-500 dark:text-gray-400 hover:text-slate-800 dark:hover:text-gray-100 hover:bg-black/5 dark:hover:bg-white/5"
|
: "text-slate-500 dark:text-gray-400 hover:text-slate-800 dark:hover:text-gray-100 hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className="w-3.5 h-3.5 flex-shrink-0" aria-hidden="true" />
|
<Icon className="w-3.5 h-3.5 flex-shrink-0" aria-hidden="true" />
|
||||||
|
|||||||
Reference in New Issue
Block a user