advance
This commit is contained in:
386
app/(protected)/teacher/actions.ts
Normal file
386
app/(protected)/teacher/actions.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
"use server";
|
||||
|
||||
import { db } from "@/lib/prisma";
|
||||
import { requireTeacher } from "@/lib/auth/requireTeacher";
|
||||
import { z } from "zod";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { ContentStatus, Prisma, ProficiencyLevel } from "@prisma/client";
|
||||
|
||||
// --- VALIDATION SCHEMAS (Zod) ---
|
||||
|
||||
const createCourseSchema = z.object({
|
||||
title: z.string().min(3, "El título debe tener al menos 3 caracteres"),
|
||||
});
|
||||
|
||||
// --- ACTIONS ---
|
||||
|
||||
/**
|
||||
* 1. LIST COURSES
|
||||
* Used by: app/(protected)/teacher/page.tsx (Dashboard)
|
||||
*/
|
||||
export async function getTeacherCourses() {
|
||||
const user = await requireTeacher();
|
||||
|
||||
if (!user) {
|
||||
return { success: false, error: "No autorizado" };
|
||||
}
|
||||
|
||||
try {
|
||||
const courses = await db.course.findMany({
|
||||
where: {
|
||||
authorId: user.id,
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: "desc",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
price: true,
|
||||
status: true,
|
||||
level: true,
|
||||
_count: {
|
||||
select: {
|
||||
modules: true,
|
||||
enrollments: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { success: true, data: courses };
|
||||
} catch (error) {
|
||||
console.error("Error fetching courses:", error);
|
||||
return { success: false, error: "Error al cargar los cursos" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 2. CREATE COURSE (Draft) */
|
||||
export async function createCourse(formData: FormData) {
|
||||
const user = await requireTeacher();
|
||||
|
||||
if (!user) {
|
||||
return { success: false, error: "No autorizado" };
|
||||
}
|
||||
|
||||
// 1. Extract & Validate
|
||||
const rawTitle = formData.get("title");
|
||||
const validated = createCourseSchema.safeParse({ title: rawTitle });
|
||||
|
||||
if (!validated.success) {
|
||||
return { success: false, error: validated.error.flatten().fieldErrors.title?.[0] || "Error en el título" };
|
||||
}
|
||||
|
||||
const title = validated.data.title;
|
||||
|
||||
// 2. Generate basic slug
|
||||
const slug = `${title.toLowerCase().replace(/ /g, "-").replace(/[^\w-]/g, "")}-${Math.floor(Math.random() * 10000)}`;
|
||||
|
||||
try {
|
||||
// 3. Create DB Record
|
||||
const course = await db.course.create({
|
||||
data: {
|
||||
title: title,
|
||||
slug: slug,
|
||||
authorId: user.id,
|
||||
description: "Descripción pendiente...",
|
||||
status: "DRAFT",
|
||||
},
|
||||
});
|
||||
|
||||
// 4. Revalidate & Return
|
||||
revalidatePath("/teacher/courses");
|
||||
return { success: true, data: course };
|
||||
|
||||
} catch (error) {
|
||||
console.error("Create course error:", error);
|
||||
return { success: false, error: "No se pudo crear el curso." };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 3. DELETE COURSE */
|
||||
export async function deleteCourse(courseId: string) {
|
||||
const user = await requireTeacher();
|
||||
|
||||
if (!user) return { success: false, error: "No autorizado" };
|
||||
|
||||
try {
|
||||
const course = await db.course.findUnique({
|
||||
where: { id: courseId },
|
||||
select: { authorId: true }
|
||||
});
|
||||
|
||||
if (!course || course.authorId !== user.id) {
|
||||
return { success: false, error: "No tienes permiso para eliminar este curso." };
|
||||
}
|
||||
|
||||
await db.course.delete({
|
||||
where: { id: courseId },
|
||||
});
|
||||
|
||||
revalidatePath("/teacher/courses");
|
||||
return { success: true, data: "Curso eliminado" };
|
||||
|
||||
} catch {
|
||||
return { success: false, error: "Error al eliminar el curso" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateCourse(courseId: string, courseSlug: string, formData: FormData) {
|
||||
const user = await requireTeacher();
|
||||
if (!user) return { success: false, error: "Unauthorized" };
|
||||
|
||||
try {
|
||||
const title = formData.get("title") as string;
|
||||
const summary = formData.get("summary") as string;
|
||||
// Direct cast is okay if you trust the select/input values
|
||||
const level = formData.get("level") as ProficiencyLevel;
|
||||
const status = formData.get("status") as ContentStatus;
|
||||
const price = parseFloat(formData.get("price") as string) || 0;
|
||||
|
||||
await db.course.update({
|
||||
where: { id: courseId, authorId: user.id },
|
||||
data: {
|
||||
title,
|
||||
description: summary,
|
||||
level,
|
||||
status,
|
||||
price,
|
||||
},
|
||||
});
|
||||
|
||||
// Revalidate both the list and the editor (edit route uses slug, not id)
|
||||
revalidatePath("/teacher/courses");
|
||||
revalidatePath(`/teacher/courses/${courseSlug}/edit`, "page");
|
||||
return { success: true };
|
||||
} catch {
|
||||
return { success: false, error: "Failed to update" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateLesson(lessonId: string, data: {
|
||||
title?: string;
|
||||
description?: Prisma.InputJsonValue | null;
|
||||
videoUrl?: string;
|
||||
isPublished?: boolean; // optional: for later
|
||||
}) {
|
||||
const user = await requireTeacher();
|
||||
if (!user) return { success: false, error: "Unauthorized" };
|
||||
|
||||
try {
|
||||
const updateData: Prisma.LessonUpdateInput = { updatedAt: new Date() };
|
||||
if (data.title !== undefined) updateData.title = data.title;
|
||||
if (data.description !== undefined) updateData.description = data.description === null ? Prisma.JsonNull : data.description;
|
||||
if (data.videoUrl !== undefined) updateData.videoUrl = data.videoUrl;
|
||||
|
||||
await db.lesson.update({
|
||||
where: { id: lessonId },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
// 2. Revalidate to show changes immediately
|
||||
revalidatePath("/teacher/courses");
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Update Lesson Error:", error);
|
||||
return { success: false, error: "Failed to update lesson" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function createModule(courseId: string) {
|
||||
const user = await requireTeacher();
|
||||
if (!user) return { success: false, error: "Unauthorized" };
|
||||
|
||||
try {
|
||||
// Find the highest orderIndex so we can put the new one at the end
|
||||
const lastModule = await db.module.findFirst({
|
||||
where: { courseId },
|
||||
orderBy: { orderIndex: "desc" },
|
||||
});
|
||||
|
||||
const newOrder = lastModule ? lastModule.orderIndex + 1 : 0;
|
||||
|
||||
await db.module.create({
|
||||
data: {
|
||||
courseId,
|
||||
title: "Nuevo Módulo", // Default title
|
||||
orderIndex: newOrder,
|
||||
},
|
||||
});
|
||||
|
||||
revalidatePath(`/teacher/courses/${courseId}/edit`, "page");
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Create Module Error:", error);
|
||||
return { success: false, error: "Failed to create module" };
|
||||
}
|
||||
}
|
||||
|
||||
// 2. CREATE LESSON
|
||||
export async function createLesson(moduleId: string) {
|
||||
const user = await requireTeacher();
|
||||
if (!user) return { success: false, error: "Unauthorized" };
|
||||
|
||||
try {
|
||||
// Find the highest orderIndex for lessons in this module
|
||||
const lastLesson = await db.lesson.findFirst({
|
||||
where: { moduleId },
|
||||
orderBy: { orderIndex: "desc" },
|
||||
});
|
||||
|
||||
const newOrder = lastLesson ? lastLesson.orderIndex + 1 : 0;
|
||||
|
||||
// Create the lesson with default values
|
||||
const lesson = await db.lesson.create({
|
||||
data: {
|
||||
moduleId,
|
||||
title: "Nueva Lección",
|
||||
orderIndex: newOrder,
|
||||
estimatedDuration: 0,
|
||||
version: 1,
|
||||
// The type field is required in your schema (based on previous context)
|
||||
// If you have a 'type' enum, set a default like 'VIDEO' or 'TEXT'
|
||||
// type: "VIDEO",
|
||||
},
|
||||
});
|
||||
|
||||
revalidatePath(`/teacher/courses/${moduleId}/edit`, "page");
|
||||
return { success: true, lessonId: lesson.id };
|
||||
} catch (error) {
|
||||
console.error("Create Lesson Error:", error);
|
||||
return { success: false, error: "Failed to create lesson" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE MODULE
|
||||
* Also deletes all lessons inside it due to Prisma cascade or manual cleanup
|
||||
*/
|
||||
export async function deleteModule(moduleId: string) {
|
||||
const user = await requireTeacher();
|
||||
if (!user) return { success: false, error: "Unauthorized" };
|
||||
|
||||
try {
|
||||
await db.module.delete({
|
||||
where: {
|
||||
id: moduleId,
|
||||
course: { authorId: user.id } // Security: ownership check
|
||||
},
|
||||
});
|
||||
|
||||
revalidatePath("/teacher/courses");
|
||||
return { success: true };
|
||||
} catch {
|
||||
return { success: false, error: "No se pudo eliminar el módulo" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE LESSON
|
||||
*/
|
||||
export async function deleteLesson(lessonId: string) {
|
||||
const user = await requireTeacher();
|
||||
if (!user) return { success: false, error: "Unauthorized" };
|
||||
|
||||
try {
|
||||
await db.lesson.delete({
|
||||
where: {
|
||||
id: lessonId,
|
||||
module: { course: { authorId: user.id } } // Deep ownership check
|
||||
},
|
||||
});
|
||||
|
||||
revalidatePath("/teacher/courses");
|
||||
return { success: true };
|
||||
} catch {
|
||||
return { success: false, error: "No se pudo eliminar la lección" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function reorderModules(moduleId: string, direction: 'up' | 'down') {
|
||||
const user = await requireTeacher();
|
||||
if (!user) return { success: false, error: "Unauthorized" };
|
||||
|
||||
const currentModule = await db.module.findUnique({
|
||||
where: { id: moduleId },
|
||||
include: { course: true }
|
||||
});
|
||||
|
||||
if (!currentModule || currentModule.course.authorId !== user.id) {
|
||||
return { success: false, error: "Module not found" };
|
||||
}
|
||||
|
||||
const siblingModule = await db.module.findFirst({
|
||||
where: {
|
||||
courseId: currentModule.courseId,
|
||||
orderIndex: direction === 'up'
|
||||
? { lt: currentModule.orderIndex }
|
||||
: { gt: currentModule.orderIndex }
|
||||
},
|
||||
orderBy: { orderIndex: direction === 'up' ? 'desc' : 'asc' }
|
||||
});
|
||||
|
||||
if (!siblingModule) return { success: true }; // Already at the top/bottom
|
||||
|
||||
// Swap orderIndex values
|
||||
await db.$transaction([
|
||||
db.module.update({
|
||||
where: { id: currentModule.id },
|
||||
data: { orderIndex: siblingModule.orderIndex }
|
||||
}),
|
||||
db.module.update({
|
||||
where: { id: siblingModule.id },
|
||||
data: { orderIndex: currentModule.orderIndex }
|
||||
})
|
||||
]);
|
||||
|
||||
revalidatePath(`/teacher/courses/${currentModule.course.slug}/edit`, "page");
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder lessons within the same module (swap orderIndex with sibling)
|
||||
*/
|
||||
export async function reorderLessons(lessonId: string, direction: "up" | "down") {
|
||||
const user = await requireTeacher();
|
||||
if (!user) return { success: false, error: "Unauthorized" };
|
||||
|
||||
const currentLesson = await db.lesson.findUnique({
|
||||
where: { id: lessonId },
|
||||
include: { module: { include: { course: true } } },
|
||||
});
|
||||
|
||||
if (!currentLesson || currentLesson.module.course.authorId !== user.id) {
|
||||
return { success: false, error: "Lesson not found" };
|
||||
}
|
||||
|
||||
const siblingLesson = await db.lesson.findFirst({
|
||||
where: {
|
||||
moduleId: currentLesson.moduleId,
|
||||
orderIndex:
|
||||
direction === "up"
|
||||
? { lt: currentLesson.orderIndex }
|
||||
: { gt: currentLesson.orderIndex },
|
||||
},
|
||||
orderBy: { orderIndex: direction === "up" ? "desc" : "asc" },
|
||||
});
|
||||
|
||||
if (!siblingLesson) return { success: true }; // Already at top/bottom
|
||||
|
||||
await db.$transaction([
|
||||
db.lesson.update({
|
||||
where: { id: currentLesson.id },
|
||||
data: { orderIndex: siblingLesson.orderIndex },
|
||||
}),
|
||||
db.lesson.update({
|
||||
where: { id: siblingLesson.id },
|
||||
data: { orderIndex: currentLesson.orderIndex },
|
||||
}),
|
||||
]);
|
||||
|
||||
revalidatePath(`/teacher/courses/${currentLesson.module.course.slug}/edit`, "page");
|
||||
return { success: true };
|
||||
}
|
||||
Reference in New Issue
Block a user