Files
ACVE/app/(protected)/my-courses/page.tsx
2026-03-15 13:52:11 +00:00

230 lines
8.1 KiB
TypeScript

import Link from "next/link";
import { redirect } from "next/navigation";
import { UserRole } from "@prisma/client";
import { requireUser } from "@/lib/auth/requireUser";
import { db } from "@/lib/prisma";
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.en === "string") return record.en;
if (typeof record.es === "string") return record.es;
}
return "";
}
export default async function MyCoursesPage() {
const user = await requireUser();
if (!user?.id) {
redirect("/auth/login?redirectTo=/my-courses");
}
const isTeacher = user.role === UserRole.TEACHER || user.role === UserRole.SUPER_ADMIN;
if (isTeacher) {
const courses = await db.course.findMany({
where: { authorId: user.id },
include: {
modules: {
include: {
lessons: {
select: {
id: true,
videoUrl: true,
youtubeUrl: true,
},
},
},
},
_count: {
select: {
enrollments: true,
},
},
},
orderBy: { updatedAt: "desc" },
});
return (
<div className="acve-page">
<section className="acve-panel acve-section-base">
<p className="acve-pill mb-3 w-fit">My Courses</p>
<h1 className="text-3xl font-semibold leading-tight text-[#222a38] md:text-4xl">Your created courses</h1>
<p className="mt-2 max-w-3xl text-base leading-relaxed text-slate-600">
Review, edit, and publish the courses you are building for your students.
</p>
</section>
{courses.length === 0 ? (
<section className="acve-panel p-6">
<h2 className="text-2xl font-semibold text-slate-900">No courses created yet</h2>
<p className="mt-2 text-slate-600">Create your first teacher course to get started.</p>
<Link className="acve-button-primary mt-4 inline-flex px-4 py-2 text-sm font-semibold" href="/teacher/courses/new">
Create course
</Link>
</section>
) : (
<section className="grid gap-4 md:grid-cols-2">
{courses.map((course) => {
const totalLessons = course.modules.reduce((acc, module) => acc + module.lessons.length, 0);
const lessonsWithVideo = course.modules.reduce(
(acc, module) => acc + module.lessons.filter((lesson) => lesson.videoUrl || lesson.youtubeUrl).length,
0,
);
const title = getText(course.title) || "Untitled course";
return (
<article key={course.id} className="acve-panel p-5">
<h2 className="text-2xl font-semibold text-[#222a38]">{title}</h2>
<p className="mt-2 text-sm text-slate-600">
{course._count.enrollments} students | {course.modules.length} modules | {totalLessons} lessons
</p>
<p className="mt-1 text-sm text-slate-600">
Upload coverage: {lessonsWithVideo}/{totalLessons || 0} lessons with video
</p>
<div className="mt-4 flex flex-wrap gap-2">
<Link
className="acve-button-primary inline-flex px-4 py-2 text-sm font-semibold"
href={`/teacher/courses/${course.slug}/edit`}
>
Edit course
</Link>
<Link
className="rounded-md border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50"
href={`/courses/${course.slug}`}
target="_blank"
>
Preview
</Link>
</div>
</article>
);
})}
</section>
)}
</div>
);
}
const enrollments = await db.enrollment.findMany({
where: { userId: user.id },
include: {
course: {
include: {
modules: {
include: {
lessons: {
select: { id: true },
},
},
},
},
},
},
orderBy: { purchasedAt: "desc" },
});
const courseIds = enrollments.map((enrollment) => enrollment.courseId);
const completed = await db.userProgress.findMany({
where: {
userId: user.id,
isCompleted: true,
lesson: {
module: {
courseId: {
in: courseIds,
},
},
},
},
select: {
lesson: {
select: {
module: {
select: { courseId: true },
},
},
},
},
});
const certificates = await db.certificate.findMany({
where: {
userId: user.id,
courseId: {
in: courseIds,
},
},
select: {
id: true,
courseId: true,
},
});
const completedByCourse = new Map<string, number>();
for (const item of completed) {
const courseId = item.lesson.module.courseId;
completedByCourse.set(courseId, (completedByCourse.get(courseId) ?? 0) + 1);
}
const certificateByCourse = new Map(certificates.map((certificate) => [certificate.courseId, certificate.id]));
return (
<div className="acve-page">
<section className="acve-panel acve-section-base">
<p className="acve-pill mb-3 w-fit">My Courses</p>
<h1 className="text-3xl font-semibold leading-tight text-[#222a38] md:text-4xl">Your enrolled courses</h1>
<p className="mt-2 max-w-3xl text-base leading-relaxed text-slate-600">
Continue where you left off, check completion, and download certificates for finished courses.
</p>
</section>
{enrollments.length === 0 ? (
<section className="acve-panel p-6">
<h2 className="text-2xl font-semibold text-slate-900">No courses enrolled yet</h2>
<p className="mt-2 text-slate-600">Browse the catalog and start your first learning path.</p>
<Link className="acve-button-primary mt-4 inline-flex px-4 py-2 text-sm font-semibold" href="/courses">
Browse courses
</Link>
</section>
) : (
<section className="grid gap-4 md:grid-cols-2">
{enrollments.map((enrollment) => {
const totalLessons = enrollment.course.modules.reduce((acc, module) => acc + module.lessons.length, 0);
const completedLessons = completedByCourse.get(enrollment.course.id) ?? 0;
const progressPercent = totalLessons > 0 ? Math.round((completedLessons / totalLessons) * 100) : 0;
const courseTitle = getText(enrollment.course.title) || "Untitled course";
const certificateId = certificateByCourse.get(enrollment.course.id);
return (
<article key={enrollment.id} className="acve-panel p-5">
<h2 className="text-2xl font-semibold text-[#222a38]">{courseTitle}</h2>
<p className="mt-2 text-sm text-slate-600">
Progress: {completedLessons}/{totalLessons} lessons ({progressPercent}%)
</p>
<div className="mt-4 flex flex-wrap gap-2">
<Link
className="acve-button-primary inline-flex px-4 py-2 text-sm font-semibold"
href={`/courses/${enrollment.course.slug}/learn`}
>
{progressPercent >= 100 ? "Review" : "Continue"}
</Link>
{certificateId ? (
<Link
className="rounded-md border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50"
href={`/api/certificates/${certificateId}/pdf`}
>
Download Certificate
</Link>
) : null}
</div>
</article>
);
})}
</section>
)}
</div>
);
}