Files
ACVE/app/(public)/courses/[slug]/learn/page.tsx
Marcelo 02afbd7cfb MVP
2026-03-20 04:54:08 +00:00

195 lines
5.6 KiB
TypeScript

import { notFound, redirect } from "next/navigation";
import { db } from "@/lib/prisma";
import { requireUser } from "@/lib/auth/requireUser";
import StudentClassroomClient from "@/components/courses/StudentClassroomClient";
import { parseLessonDescriptionMeta } from "@/lib/courses/lessonContent";
type LessonSelect = {
id: string;
title: unknown;
description: unknown;
videoUrl: string | null;
youtubeUrl: string | null;
estimatedDuration: number;
isFreePreview: boolean;
};
type ModuleSelect = { id: string; title: unknown; lessons: LessonSelect[] };
type CourseWithModules = { id: string; slug: string; title: unknown; price: unknown; modules: ModuleSelect[] };
function getText(value: unknown): string {
if (!value) return "";
if (typeof value === "string") return value;
if (typeof value === "object") {
const record = value as Record<string, unknown>;
if (typeof record.es === "string") return record.es;
if (typeof record.en === "string") return record.en;
}
return "";
}
type PageProps = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ lesson?: string }>;
};
export default async function CourseLearnPage({ params, searchParams }: PageProps) {
const { slug } = await params;
const { lesson: requestedLessonId } = await searchParams;
const user = await requireUser();
const courseSelect = {
id: true,
slug: true,
title: true,
price: true,
modules: {
orderBy: { orderIndex: "asc" as const },
select: {
id: true,
title: true,
lessons: {
orderBy: { orderIndex: "asc" as const },
select: {
id: true,
title: true,
description: true,
videoUrl: true,
youtubeUrl: true,
estimatedDuration: true,
isFreePreview: true,
},
},
},
},
};
const course = (await db.course.findUnique({
where: { slug },
select: courseSelect,
})) as CourseWithModules | null;
if (!course) {
notFound();
}
let enrollment: { id: string } | null = null;
let isEnrolled: boolean;
if (!user?.id) {
// Anonymous: no enrollment, preview-only access
const allLessons = course.modules.flatMap((m) =>
m.lessons.map((l) => ({ id: l.id, isFreePreview: l.isFreePreview }))
);
const firstPreviewLesson = allLessons.find((l) => l.isFreePreview);
if (!firstPreviewLesson) {
redirect(`/courses/${slug}`);
}
isEnrolled = false;
} else {
enrollment = await db.enrollment.findUnique({
where: {
userId_courseId: {
userId: user.id,
courseId: course.id,
},
},
select: { id: true },
});
const isFree = Number(course.price) === 0;
if (!enrollment) {
if (isFree) {
enrollment = await db.enrollment.create({
data: {
userId: user.id,
courseId: course.id,
amountPaid: 0,
},
select: { id: true },
});
isEnrolled = true;
} else {
// Paid course, no enrollment: allow only if there are preview lessons
const allLessons = course.modules.flatMap((m) =>
m.lessons.map((l) => ({ id: l.id, isFreePreview: l.isFreePreview }))
);
const firstPreviewLesson = allLessons.find((l) => l.isFreePreview);
if (!firstPreviewLesson) {
redirect(`/courses/${slug}`);
}
isEnrolled = false;
}
} else {
isEnrolled = true;
}
}
const completedProgress =
isEnrolled && user
? await db.userProgress.findMany({
where: {
userId: user.id,
isCompleted: true,
lesson: {
module: { courseId: course.id },
},
},
select: { lessonId: true },
})
: [];
const modules = course.modules.map((module) => ({
id: module.id,
title: getText(module.title) || "Untitled module",
lessons: module.lessons.map((lesson) => {
const lessonMeta = parseLessonDescriptionMeta(lesson.description);
return {
id: lesson.id,
title: getText(lesson.title) || "Untitled lesson",
description: lessonMeta.text,
contentType: lessonMeta.contentType,
lectureContent: lessonMeta.lectureContent,
activity: lessonMeta.activity,
materialUrl: lessonMeta.materialUrl,
videoUrl: lesson.videoUrl,
youtubeUrl: lesson.youtubeUrl,
estimatedDuration: lesson.estimatedDuration,
isFreePreview: lesson.isFreePreview,
};
}),
}));
const flattenedLessons = modules.flatMap((module) => module.lessons);
const flattenedLessonIds = flattenedLessons.map((l) => l.id);
let initialSelectedLessonId: string;
if (isEnrolled) {
initialSelectedLessonId =
requestedLessonId && flattenedLessonIds.includes(requestedLessonId)
? requestedLessonId
: flattenedLessonIds[0] ?? "";
} else {
const firstPreview = flattenedLessons.find((l) => l.isFreePreview);
const requestedLesson = requestedLessonId
? flattenedLessons.find((l) => l.id === requestedLessonId)
: null;
if (requestedLesson?.isFreePreview) {
initialSelectedLessonId = requestedLessonId!;
} else {
initialSelectedLessonId = firstPreview?.id ?? "";
}
}
return (
<StudentClassroomClient
courseSlug={course.slug}
courseTitle={getText(course.title) || "Untitled course"}
modules={modules}
initialSelectedLessonId={initialSelectedLessonId}
initialCompletedLessonIds={completedProgress.map((p) => p.lessonId)}
isEnrolled={isEnrolled}
/>
);
}