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>
This commit is contained in:
@@ -14,7 +14,12 @@ export default async function StudentDashboard() {
|
||||
course: {
|
||||
include: {
|
||||
modules: {
|
||||
include: { _count: { select: { lessons: true } } },
|
||||
include: {
|
||||
lessons: {
|
||||
where: { published: true },
|
||||
select: { id: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
_count: { select: { modules: true } },
|
||||
},
|
||||
@@ -27,6 +32,18 @@ export default async function StudentDashboard() {
|
||||
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>
|
||||
@@ -45,7 +62,14 @@ export default async function StudentDashboard() {
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{active.map(({ course, expiresAt }) => {
|
||||
const totalLessons = course.modules.reduce((s, m) => s + m._count.lessons, 0);
|
||||
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}
|
||||
@@ -60,23 +84,46 @@ export default async function StudentDashboard() {
|
||||
📚
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4 flex-1">
|
||||
<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 mt-1 line-clamp-2" style={{ color: "var(--muted-foreground)" }}>
|
||||
<p className="text-xs line-clamp-2" style={{ color: "var(--muted-foreground)" }}>
|
||||
{course.description}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<span className="text-xs" style={{ color: "var(--muted-foreground)" }}>
|
||||
{course._count.modules} модулей · {totalLessons} уроков
|
||||
</span>
|
||||
{expiresAt && (
|
||||
<span className="text-xs" style={{ color: "var(--muted-foreground)" }}>
|
||||
до {new Date(expiresAt).toLocaleDateString("ru-RU")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user