Files
lms-sb/src/app/(student)/dashboard/page.tsx
T
admins d0c8c6dd53 Add lesson progress tracking
- Toggle lesson completion via server action (LessonProgress table)
- "Отметить как пройденный" button on lesson page, turns accent when done
- Course sidebar: progress bar, checkmarks on completed lessons, X/Y counter per module
- Dashboard: progress bar on each course card with completion percentage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 13:16:28 +05:00

154 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { headers } from "next/headers";
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
import { prisma } from "@/lib/prisma";
import Link from "next/link";
export default async function StudentDashboard() {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) redirect("/login");
const enrollments = await prisma.courseEnrollment.findMany({
where: { userId: session.user.id },
include: {
course: {
include: {
modules: {
include: {
lessons: {
where: { published: true },
select: { id: true },
},
},
},
_count: { select: { modules: true } },
},
},
},
orderBy: { enrolledAt: "desc" },
});
const now = new Date();
const active = enrollments.filter((e) => !e.expiresAt || e.expiresAt > now);
const expired = enrollments.filter((e) => e.expiresAt && e.expiresAt <= now);
// Fetch progress for all lessons in active courses
const allLessonIds = active.flatMap((e) =>
e.course.modules.flatMap((m) => m.lessons.map((l) => l.id))
);
const progressRecords = allLessonIds.length > 0
? await prisma.lessonProgress.findMany({
where: { userId: session.user.id, lessonId: { in: allLessonIds } },
select: { lessonId: true },
})
: [];
const completedSet = new Set(progressRecords.map((p) => p.lessonId));
return (
<main className="max-w-4xl mx-auto px-6 py-10 w-full">
<h1 className="text-2xl font-bold mb-1">Мои курсы</h1>
<p className="text-sm mb-8" style={{ color: "var(--muted-foreground)" }}>
{active.length} активных курсов
</p>
{active.length === 0 ? (
<div className="card-aubade p-12 text-center">
<p className="text-4xl mb-3">📚</p>
<p className="font-medium">Доступных курсов пока нет</p>
<p className="text-sm mt-1" style={{ color: "var(--muted-foreground)" }}>
Обратитесь к администратору за доступом
</p>
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2">
{active.map(({ course, expiresAt }) => {
const totalLessons = course.modules.reduce((s, m) => s + m.lessons.length, 0);
const completedLessons = course.modules
.flatMap((m) => m.lessons)
.filter((l) => completedSet.has(l.id)).length;
const progressPct = totalLessons > 0
? Math.round((completedLessons / totalLessons) * 100)
: 0;
return (
<Link
key={course.id}
href={`/courses/${course.slug}`}
className="card-aubade p-0 overflow-hidden flex flex-col"
>
{course.coverImage ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={course.coverImage} alt={course.title} className="w-full aspect-video object-cover" />
) : (
<div className="w-full aspect-video flex items-center justify-center text-4xl" style={{ backgroundColor: "var(--color-surface)" }}>
📚
</div>
)}
<div className="p-4 flex-1 flex flex-col gap-2">
<h2 className="font-bold text-base leading-tight">{course.title}</h2>
{course.description && (
<p className="text-xs line-clamp-2" style={{ color: "var(--muted-foreground)" }}>
{course.description}
</p>
)}
{/* Progress bar */}
{totalLessons > 0 && (
<div className="mt-auto">
<div className="flex items-center justify-between mb-1">
<span className="text-xs" style={{ color: "var(--muted-foreground)" }}>
{completedLessons} из {totalLessons} уроков
</span>
<span
className="text-xs font-bold"
style={{ color: progressPct === 100 ? "var(--foreground)" : "var(--muted-foreground)" }}
>
{progressPct === 100 ? "✓ Завершён" : `${progressPct}%`}
</span>
</div>
<div className="h-1.5 w-full" style={{ background: "var(--border)" }}>
<div
className="h-full transition-all"
style={{
width: `${progressPct}%`,
background: progressPct === 100 ? "var(--foreground)" : "var(--accent)",
border: progressPct > 0 ? "1px solid var(--foreground)" : "none",
}}
/>
</div>
</div>
)}
{expiresAt && (
<p className="text-xs" style={{ color: "var(--muted-foreground)" }}>
Доступ до {new Date(expiresAt).toLocaleDateString("ru-RU")}
</p>
)}
</div>
</Link>
);
})}
</div>
)}
{expired.length > 0 && (
<div className="mt-10">
<p className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: "var(--muted-foreground)" }}>
Доступ истёк
</p>
<div className="space-y-2">
{expired.map(({ course, expiresAt }) => (
<div key={course.id} className="flex items-center justify-between px-4 py-3 opacity-50" style={{ border: "2px solid var(--border)" }}>
<span className="text-sm font-medium">{course.title}</span>
<span className="text-xs" style={{ color: "oklch(0.577 0.245 27.325)" }}>
Истёк {new Date(expiresAt!).toLocaleDateString("ru-RU")}
</span>
</div>
))}
</div>
</div>
)}
</main>
);
}