193 lines
5.5 KiB
TypeScript
193 lines
5.5 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,
|
|
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}
|
|
/>
|
|
);
|
|
}
|