-
Login
-
Sign in to access protected learning routes.
+
+
+ {isTeacher ? "Acceso Profesores" : "Iniciar Sesión"}
+
+
+ {isTeacher
+ ? "Gestiona tus cursos y estudiantes."
+ : "Ingresa para continuar aprendiendo."}
+
-
-
- New here?{" "}
-
- Create an account
-
-
+
+
+
+ ¿Olvidaste tu contraseña?
+
+
+
+ ¿Nuevo aquí?{" "}
+
+ Crear cuenta
+
+
+ {!isTeacher && (
+
+ ¿Eres profesor?{" "}
+
+ Ingresa aquí
+
+
+ )}
+
);
-}
+}
\ No newline at end of file
diff --git a/components/courses/StudentClassroomClient.tsx b/components/courses/StudentClassroomClient.tsx
new file mode 100644
index 0000000..eab831d
--- /dev/null
+++ b/components/courses/StudentClassroomClient.tsx
@@ -0,0 +1,218 @@
+"use client";
+
+import { useEffect, useMemo, useState, useTransition } from "react";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { Check, Lock, PlayCircle } from "lucide-react";
+import { toggleLessonComplete } from "@/app/(protected)/courses/[slug]/learn/actions";
+import ProgressBar from "@/components/ProgressBar";
+
+type ClassroomLesson = {
+ id: string;
+ title: string;
+ description: string;
+ videoUrl: string | null;
+ estimatedDuration: number;
+};
+
+type ClassroomModule = {
+ id: string;
+ title: string;
+ lessons: ClassroomLesson[];
+};
+
+type StudentClassroomClientProps = {
+ courseSlug: string;
+ courseTitle: string;
+ modules: ClassroomModule[];
+ initialSelectedLessonId: string;
+ initialCompletedLessonIds: string[];
+};
+
+export default function StudentClassroomClient({
+ courseSlug,
+ courseTitle,
+ modules,
+ initialSelectedLessonId,
+ initialCompletedLessonIds,
+}: StudentClassroomClientProps) {
+ const router = useRouter();
+ const [isSaving, startTransition] = useTransition();
+ const [selectedLessonId, setSelectedLessonId] = useState(initialSelectedLessonId);
+ const [completedLessonIds, setCompletedLessonIds] = useState(initialCompletedLessonIds);
+
+ useEffect(() => {
+ setSelectedLessonId(initialSelectedLessonId);
+ }, [initialSelectedLessonId]);
+
+ useEffect(() => {
+ setCompletedLessonIds(initialCompletedLessonIds);
+ }, [initialCompletedLessonIds]);
+
+ const flatLessons = useMemo(() => modules.flatMap((module) => module.lessons), [modules]);
+ const completedSet = useMemo(() => new Set(completedLessonIds), [completedLessonIds]);
+
+ const totalLessons = flatLessons.length;
+ const completedCount = completedLessonIds.length;
+ const progressPercent = totalLessons > 0 ? Math.round((completedCount / totalLessons) * 100) : 0;
+
+ const selectedLesson =
+ flatLessons.find((lesson) => lesson.id === selectedLessonId) ?? flatLessons[0] ?? null;
+
+ const isRestricted = (lessonId: string) => {
+ const lessonIndex = flatLessons.findIndex((lesson) => lesson.id === lessonId);
+ if (lessonIndex <= 0) return false;
+ if (completedSet.has(lessonId)) return false;
+ const previousLesson = flatLessons[lessonIndex - 1];
+ return !completedSet.has(previousLesson.id);
+ };
+
+ const navigateToLesson = (lessonId: string) => {
+ if (isRestricted(lessonId)) return;
+ setSelectedLessonId(lessonId);
+ router.push(`/courses/${courseSlug}/learn?lesson=${lessonId}`, { scroll: false });
+ };
+
+ const handleToggleComplete = async () => {
+ if (!selectedLesson || isSaving) return;
+
+ const lessonId = selectedLesson.id;
+ const wasCompleted = completedSet.has(lessonId);
+
+ setCompletedLessonIds((prev) =>
+ wasCompleted ? prev.filter((id) => id !== lessonId) : [...prev, lessonId],
+ );
+
+ startTransition(async () => {
+ const result = await toggleLessonComplete({ courseSlug, lessonId });
+
+ if (!result.success) {
+ setCompletedLessonIds((prev) =>
+ wasCompleted ? [...prev, lessonId] : prev.filter((id) => id !== lessonId),
+ );
+ return;
+ }
+
+ setCompletedLessonIds((prev) => {
+ if (result.isCompleted) {
+ return prev.includes(lessonId) ? prev : [...prev, lessonId];
+ }
+ return prev.filter((id) => id !== lessonId);
+ });
+
+ router.refresh();
+ });
+ };
+
+ if (!selectedLesson) {
+ return (
+
+
No lessons available yet
+
This course does not have lessons configured.
+
+ );
+ }
+
+ return (
+
+
+
+ {"<-"} Back to Course
+
+
+
+ {selectedLesson.videoUrl ? (
+
+ ) : (
+
+ Video not available for this lesson
+
+ )}
+
+
+
+
{selectedLesson.title}
+ {selectedLesson.description ? (
+
{selectedLesson.description}
+ ) : null}
+
+ Duration: {Math.max(1, Math.ceil(selectedLesson.estimatedDuration / 60))} min
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/teacher/TeacherDashboardClient.tsx b/components/teacher/TeacherDashboardClient.tsx
old mode 100644
new mode 100755
index 80b2047..3fd377d
--- a/components/teacher/TeacherDashboardClient.tsx
+++ b/components/teacher/TeacherDashboardClient.tsx
@@ -15,6 +15,13 @@ export default function TeacherDashboardClient() {
return () => window.removeEventListener(teacherCoursesUpdatedEventName, load);
}, []);
+ const totalLessons = courses.reduce((sum, course) => sum + course.lessons.length, 0);
+ const totalStudents = courses.reduce((sum, course) => sum + course.students, 0);
+ const averageRating =
+ courses.length === 0 ? 0 : courses.reduce((sum, course) => sum + course.rating, 0) / courses.length;
+ const publishedCount = courses.filter((course) => course.status === "Published").length;
+ const draftCount = courses.filter((course) => course.status === "Draft").length;
+
return (
@@ -22,14 +29,39 @@ export default function TeacherDashboardClient() {
Teacher Dashboard
Manage teacher-created courses stored locally for MVP.
-
- Create course
-
+
+
+ Upload library
+
+
+ Create course
+
+
+
+
+ Courses
+ {courses.length}
+ Published: {publishedCount} | Draft: {draftCount}
+
+
+ Lessons
+ {totalLessons}
+
+
+ Avg. rating
+ {averageRating.toFixed(1)}
+ Students tracked: {totalStudents}
+
+
+
{courses.length === 0 ? (
No teacher-created courses yet.
@@ -41,6 +73,7 @@ export default function TeacherDashboardClient() {
{course.level}
+
{course.status}
{course.title}
{course.summary}
diff --git a/components/teacher/TeacherEditCourseForm.tsx b/components/teacher/TeacherEditCourseForm.tsx
old mode 100644
new mode 100755
index cdcb23f..8a6c166
--- a/components/teacher/TeacherEditCourseForm.tsx
+++ b/components/teacher/TeacherEditCourseForm.tsx
@@ -1,176 +1,373 @@
+// components/teacher/TeacherEditCourseForm.tsx
"use client";
-import Link from "next/link";
-import { FormEvent, useEffect, useState } from "react";
+import {
+ updateCourse,
+ createModule,
+ createLesson,
+ reorderModules,
+ reorderLessons,
+} from "@/app/(protected)/teacher/actions";
+import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
-import { getTeacherCourseBySlug, teacherCoursesUpdatedEventName, updateTeacherCourse } from "@/lib/data/teacherCourses";
-import type { Course, CourseLevel } from "@/types/course";
+import Link from "next/link";
+import { toast } from "sonner";
-const levels: CourseLevel[] = ["Beginner", "Intermediate", "Advanced"];
+import { Prisma } from "@prisma/client";
-type TeacherEditCourseFormProps = {
+type CourseData = {
+ id: string;
slug: string;
+ title: Prisma.JsonValue;
+ description: Prisma.JsonValue;
+ level: string;
+ status: string;
+ price: number;
+ modules: {
+ id: string;
+ title: Prisma.JsonValue;
+ lessons: { id: string; title: Prisma.JsonValue }[];
+ }[];
};
-export default function TeacherEditCourseForm({ slug }: TeacherEditCourseFormProps) {
+export default function TeacherEditCourseForm({ course }: { course: CourseData }) {
const router = useRouter();
- const [course, setCourse] = useState
(null);
- const [title, setTitle] = useState("");
- const [level, setLevel] = useState("Beginner");
- const [summary, setSummary] = useState("");
- const [instructor, setInstructor] = useState("");
- const [weeks, setWeeks] = useState(4);
- const [rating, setRating] = useState(5);
- const [saved, setSaved] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [optimisticModules, setOptimisticModules] = useState(course.modules);
useEffect(() => {
- const load = () => {
- const found = getTeacherCourseBySlug(slug) ?? null;
- setCourse(found);
- if (!found) return;
- setTitle(found.title);
- setLevel(found.level);
- setSummary(found.summary);
- setInstructor(found.instructor);
- setWeeks(found.weeks);
- setRating(found.rating);
- };
+ setOptimisticModules(course.modules);
+ }, [course.modules]);
- load();
- window.addEventListener(teacherCoursesUpdatedEventName, load);
- return () => window.removeEventListener(teacherCoursesUpdatedEventName, load);
- }, [slug]);
-
- const submit = (event: FormEvent) => {
- event.preventDefault();
- if (!course) return;
-
- updateTeacherCourse(course.slug, {
- title: title.trim(),
- level,
- summary: summary.trim(),
- instructor: instructor.trim(),
- weeks: Math.max(1, weeks),
- rating: Math.min(5, Math.max(0, rating)),
- });
- setSaved(true);
- window.setTimeout(() => setSaved(false), 1200);
+ // Helper for JSON/String fields
+ const getStr = (val: Prisma.JsonValue) => {
+ if (typeof val === "string") return val;
+ if (val && typeof val === "object" && !Array.isArray(val)) {
+ const v = val as Record;
+ if (typeof v.en === "string") return v.en;
+ if (typeof v.es === "string") return v.es;
+ return "";
+ }
+ return "";
};
- if (!course) {
- return (
-
-
Teacher course not found
-
This editor only works for courses created in the teacher area.
-
- Back to dashboard
-
-
- );
+ // 1. SAVE COURSE SETTINGS
+ async function handleSubmit(formData: FormData) {
+ setLoading(true);
+ const res = await updateCourse(course.id, course.slug, formData);
+
+ if (res.success) {
+ toast.success("Curso actualizado");
+ router.refresh();
+ } else {
+ toast.error("Error al guardar");
+ }
+ setLoading(false);
}
+ // 2. CREATE NEW MODULE
+ const handleAddModule = async () => {
+ setLoading(true); // Block UI while working
+ const res = await createModule(course.id);
+ if (res.success) {
+ toast.success("Módulo agregado");
+ router.refresh();
+ } else {
+ toast.error("Error al crear módulo");
+ }
+ setLoading(false);
+ };
+
+ // 3. CREATE NEW LESSON
+ const handleAddLesson = async (moduleId: string) => {
+ setLoading(true);
+ const res = await createLesson(moduleId);
+ if (res.success && res.lessonId) {
+ toast.success("Lección creada");
+ // Redirect immediately to the video upload page
+ router.push(`/teacher/courses/${course.slug}/lessons/${res.lessonId}`);
+ } else {
+ toast.error("Error al crear lección");
+ setLoading(false); // Only stop loading if we failed (otherwise we are redirecting)
+ }
+ };
+
+ // 4. REORDER MODULES (optimistic)
+ const handleReorderModule = async (moduleIndex: number, direction: "up" | "down") => {
+ const swapWith = direction === "up" ? moduleIndex - 1 : moduleIndex + 1;
+ if (swapWith < 0 || swapWith >= optimisticModules.length) return;
+
+ const next = [...optimisticModules];
+ [next[moduleIndex], next[swapWith]] = [next[swapWith], next[moduleIndex]];
+ setOptimisticModules(next);
+
+ const res = await reorderModules(optimisticModules[moduleIndex].id, direction);
+ if (res.success) {
+ router.refresh();
+ } else {
+ setOptimisticModules(course.modules);
+ toast.error(res.error ?? "Error al reordenar");
+ }
+ };
+
+ // 5. REORDER LESSONS (optimistic)
+ const handleReorderLesson = async (
+ moduleIndex: number,
+ lessonIndex: number,
+ direction: "up" | "down"
+ ) => {
+ const lessons = optimisticModules[moduleIndex].lessons;
+ const swapWith = direction === "up" ? lessonIndex - 1 : lessonIndex + 1;
+ if (swapWith < 0 || swapWith >= lessons.length) return;
+
+ const nextModules = [...optimisticModules];
+ const nextLessons = [...lessons];
+ [nextLessons[lessonIndex], nextLessons[swapWith]] = [
+ nextLessons[swapWith],
+ nextLessons[lessonIndex],
+ ];
+ nextModules[moduleIndex] = { ...nextModules[moduleIndex], lessons: nextLessons };
+ setOptimisticModules(nextModules);
+
+ const res = await reorderLessons(lessons[lessonIndex].id, direction);
+ if (res.success) {
+ router.refresh();
+ } else {
+ setOptimisticModules(course.modules);
+ toast.error(res.error ?? "Error al reordenar");
+ }
+ };
+
return (
-