230 lines
8.1 KiB
TypeScript
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>
|
|
);
|
|
}
|