Files
lms-sb/src/components/student/course-sidebar.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

160 lines
5.4 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.
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useState } from "react";
interface Lesson {
id: string;
title: string;
}
interface Module {
id: string;
title: string;
lessons: Lesson[];
}
interface Course {
slug: string;
title: string;
modules: Module[];
}
export function CourseSidebar({
course,
completedLessonIds = new Set(),
}: {
course: Course;
completedLessonIds?: Set<string>;
}) {
const pathname = usePathname();
const [open, setOpen] = useState(true);
const totalLessons = course.modules.reduce((s, m) => s + m.lessons.length, 0);
const completedCount = course.modules
.flatMap((m) => m.lessons)
.filter((l) => completedLessonIds.has(l.id)).length;
const progressPct = totalLessons > 0 ? Math.round((completedCount / totalLessons) * 100) : 0;
return (
<>
{/* Mobile toggle */}
<button
className="md:hidden fixed bottom-4 right-4 z-20 btn-aubade px-3 py-2 text-sm"
onClick={() => setOpen(!open)}
>
{open ? "✕" : "☰ Уроки"}
</button>
<aside
className={`w-64 shrink-0 flex flex-col overflow-y-auto ${open ? "flex" : "hidden md:flex"}`}
style={{
borderRight: "2px solid var(--border)",
backgroundColor: "var(--background)",
maxHeight: "calc(100vh - 53px)",
position: "sticky",
top: "53px",
}}
>
{/* Course title + progress */}
<div className="px-4 py-4" style={{ borderBottom: "2px solid var(--border)" }}>
<Link
href={`/courses/${course.slug}`}
className="font-bold text-sm leading-snug block mb-1"
style={{ color: "var(--foreground)" }}
>
{course.title}
</Link>
<Link
href="/dashboard"
className="text-xs block underline mb-3"
style={{ color: "var(--muted-foreground)" }}
>
Все курсы
</Link>
{totalLessons > 0 && (
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-xs" style={{ color: "var(--muted-foreground)" }}>
{completedCount} из {totalLessons} уроков
</span>
<span className="text-xs font-bold" style={{ color: "var(--foreground)" }}>
{progressPct}%
</span>
</div>
<div
className="h-1.5 w-full"
style={{ background: "var(--border)" }}
>
<div
className="h-full transition-all duration-300"
style={{
width: `${progressPct}%`,
background: progressPct === 100 ? "var(--foreground)" : "var(--accent)",
border: progressPct > 0 ? "1px solid var(--foreground)" : "none",
}}
/>
</div>
</div>
)}
</div>
{/* Modules and lessons */}
<nav className="flex-1 py-2">
{course.modules.map((mod) => {
const modCompleted = mod.lessons.filter((l) => completedLessonIds.has(l.id)).length;
return (
<div key={mod.id}>
<div className="flex items-center justify-between px-4 py-2">
<p
className="text-xs font-bold uppercase tracking-widest"
style={{ color: "var(--muted-foreground)" }}
>
{mod.title}
</p>
{mod.lessons.length > 0 && (
<span className="text-xs" style={{ color: "var(--muted-foreground)" }}>
{modCompleted}/{mod.lessons.length}
</span>
)}
</div>
{mod.lessons.map((lesson) => {
const active = pathname.includes(lesson.id);
const done = completedLessonIds.has(lesson.id);
return (
<Link
key={lesson.id}
href={`/courses/${course.slug}/lessons/${lesson.id}`}
className="flex items-center gap-2 px-4 py-2 text-sm leading-snug border-l-2 transition-colors"
style={{
borderLeftColor: active ? "var(--foreground)" : "transparent",
backgroundColor: active ? "var(--color-highlight)" : "transparent",
fontWeight: active ? 600 : 400,
color: done && !active ? "var(--muted-foreground)" : "var(--foreground)",
}}
>
<span
className="shrink-0 w-4 h-4 flex items-center justify-center text-xs"
style={{
border: `1.5px solid ${done ? "var(--foreground)" : "var(--border)"}`,
background: done ? "var(--foreground)" : "transparent",
color: "var(--background)",
}}
>
{done && "✓"}
</span>
<span className="flex-1 leading-snug">{lesson.title}</span>
</Link>
);
})}
</div>
);
})}
</nav>
</aside>
</>
);
}