d0c8c6dd53
- 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>
160 lines
5.4 KiB
TypeScript
160 lines
5.4 KiB
TypeScript
"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>
|
||
</>
|
||
);
|
||
}
|