Pending course, rest ready for launch

This commit is contained in:
Marcelo
2026-03-15 13:52:11 +00:00
parent be4ca2ed78
commit 62b3cfe467
77 changed files with 6450 additions and 868 deletions

View File

@@ -5,6 +5,11 @@ import { requireTeacher } from "@/lib/auth/requireTeacher";
import { z } from "zod";
import { revalidatePath } from "next/cache";
import { ContentStatus, Prisma, ProficiencyLevel } from "@prisma/client";
import {
buildLessonDescriptionMeta,
parseLessonDescriptionMeta,
type LessonContentType,
} from "@/lib/courses/lessonContent";
// --- VALIDATION SCHEMAS (Zod) ---
@@ -129,6 +134,17 @@ export async function deleteCourse(courseId: string) {
}
}
function parseLearningOutcomes(raw: FormDataEntryValue | null): string[] {
if (raw == null || typeof raw !== "string") return [];
try {
const parsed = JSON.parse(raw) as unknown;
if (!Array.isArray(parsed)) return [];
return parsed.filter((x): x is string => typeof x === "string");
} catch {
return [];
}
}
export async function updateCourse(courseId: string, courseSlug: string, formData: FormData) {
const user = await requireTeacher();
if (!user) return { success: false, error: "Unauthorized" };
@@ -140,6 +156,7 @@ export async function updateCourse(courseId: string, courseSlug: string, formDat
const level = formData.get("level") as ProficiencyLevel;
const status = formData.get("status") as ContentStatus;
const price = parseFloat(formData.get("price") as string) || 0;
const learningOutcomes = parseLearningOutcomes(formData.get("learningOutcomes"));
await db.course.update({
where: { id: courseId, authorId: user.id },
@@ -149,12 +166,16 @@ export async function updateCourse(courseId: string, courseSlug: string, formDat
level,
status,
price,
},
learningOutcomes:
learningOutcomes.length > 0 ? (learningOutcomes as Prisma.InputJsonValue) : Prisma.JsonNull,
} as Prisma.CourseUpdateInput,
});
// Revalidate both the list and the editor (edit route uses slug, not id)
// Revalidate teacher list, editor page + layout (so router.refresh() gets fresh data), and public catalog
revalidatePath("/teacher/courses");
revalidatePath(`/teacher/courses/${courseSlug}/edit`, "page");
revalidatePath(`/teacher/courses/${courseSlug}/edit`, "layout");
revalidatePath("/courses");
return { success: true };
} catch {
return { success: false, error: "Failed to update" };
@@ -163,8 +184,13 @@ export async function updateCourse(courseId: string, courseSlug: string, formDat
export async function updateLesson(lessonId: string, data: {
title?: string;
description?: Prisma.InputJsonValue | null;
description?: string;
videoUrl?: string;
youtubeUrl?: string;
materialUrl?: string;
contentType?: LessonContentType;
estimatedDurationMinutes?: number;
isPreview?: boolean; // maps to DB field isFreePreview
isPublished?: boolean; // optional: for later
}) {
const user = await requireTeacher();
@@ -173,8 +199,30 @@ export async function updateLesson(lessonId: string, data: {
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;
if (data.youtubeUrl !== undefined) updateData.youtubeUrl = data.youtubeUrl;
if (data.estimatedDurationMinutes !== undefined) {
const minutes = Math.max(0, Math.round(data.estimatedDurationMinutes));
updateData.estimatedDuration = minutes * 60;
}
if (data.isPreview !== undefined) updateData.isFreePreview = data.isPreview;
const shouldUpdateMeta =
data.description !== undefined || data.contentType !== undefined || data.materialUrl !== undefined;
if (shouldUpdateMeta) {
const lesson = await db.lesson.findUnique({
where: { id: lessonId },
select: { description: true },
});
const existingMeta = parseLessonDescriptionMeta(lesson?.description);
updateData.description = buildLessonDescriptionMeta({
text: data.description ?? existingMeta.text,
contentType: data.contentType ?? existingMeta.contentType,
materialUrl: data.materialUrl ?? existingMeta.materialUrl,
});
}
await db.lesson.update({
where: { id: lessonId },
@@ -211,7 +259,7 @@ export async function createModule(courseId: string) {
},
});
revalidatePath(`/teacher/courses/${courseId}/edit`, "page");
revalidatePath("/teacher/courses");
return { success: true };
} catch (error) {
console.error("Create Module Error:", error);
@@ -219,6 +267,38 @@ export async function createModule(courseId: string) {
}
}
/**
* UPDATE MODULE TITLE
*/
export async function updateModuleTitle(moduleId: string, title: string) {
const user = await requireTeacher();
if (!user) return { success: false, error: "Unauthorized" };
const trimmed = title?.trim() || "";
if (!trimmed) return { success: false, error: "El título no puede estar vacío" };
try {
const moduleRow = await db.module.findFirst({
where: { id: moduleId, course: { authorId: user.id } },
select: { id: true, course: { select: { slug: true } } },
});
if (!moduleRow) return { success: false, error: "Módulo no encontrado" };
await db.module.update({
where: { id: moduleId },
data: { title: trimmed },
});
revalidatePath("/teacher/courses");
revalidatePath(`/teacher/courses/${moduleRow.course.slug}/edit`, "page");
revalidatePath(`/teacher/courses/${moduleRow.course.slug}/edit`, "layout");
return { success: true };
} catch (error) {
console.error("Update Module Title Error:", error);
return { success: false, error: "No se pudo actualizar el título" };
}
}
// 2. CREATE LESSON
export async function createLesson(moduleId: string) {
const user = await requireTeacher();
@@ -238,6 +318,9 @@ export async function createLesson(moduleId: string) {
data: {
moduleId,
title: "Nueva Lección",
description: {
contentType: "VIDEO",
},
orderIndex: newOrder,
estimatedDuration: 0,
version: 1,
@@ -383,4 +466,4 @@ export async function reorderLessons(lessonId: string, direction: "up" | "down")
revalidatePath(`/teacher/courses/${currentLesson.module.course.slug}/edit`, "page");
return { success: true };
}
}