Pending course, rest ready for launch
This commit is contained in:
229
app/(protected)/my-courses/page.tsx
Normal file
229
app/(protected)/my-courses/page.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user