feat: collapsible mobile sidebar for student course view

Hamburger button (top-left, lg:hidden), dark overlay, slide-in animation.
Sidebar closes on lesson link click. Spacer added to prevent content overlap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-14 05:07:29 +00:00
parent e3e6c713d2
commit 47840901c5
2 changed files with 28 additions and 7 deletions
@@ -62,6 +62,7 @@ export default async function CourseLayout({ children, params }: Props) {
<div className="flex flex-1"> <div className="flex flex-1">
<CourseSidebar course={course} completedLessonIds={completedLessonIds} /> <CourseSidebar course={course} completedLessonIds={completedLessonIds} />
<main className="flex-1 min-w-0 overflow-y-auto"> <main className="flex-1 min-w-0 overflow-y-auto">
<div className="h-12 lg:hidden" />
{children} {children}
</main> </main>
</div> </div>
+27 -7
View File
@@ -3,6 +3,7 @@
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { Menu, X } from "lucide-react";
interface Lesson { interface Lesson {
id: string; id: string;
@@ -29,7 +30,7 @@ export function CourseSidebar({
completedLessonIds?: Set<string>; completedLessonIds?: Set<string>;
}) { }) {
const pathname = usePathname(); const pathname = usePathname();
const [open, setOpen] = useState(true); const [open, setOpen] = useState(false);
const totalLessons = course.modules.reduce((s, m) => s + m.lessons.length, 0); const totalLessons = course.modules.reduce((s, m) => s + m.lessons.length, 0);
const completedCount = course.modules const completedCount = course.modules
@@ -39,21 +40,39 @@ export function CourseSidebar({
return ( return (
<> <>
{/* Mobile toggle */} {/* Mobile hamburger */}
<button <button
className="md:hidden fixed bottom-4 right-4 z-20 btn-aubade px-3 py-2 text-sm" className="lg:hidden fixed top-3 left-3 z-50 p-2 rounded"
onClick={() => setOpen(!open)} style={{
backgroundColor: "var(--background)",
border: "1.5px solid var(--border)",
color: "var(--foreground)",
}}
onClick={() => setOpen((v) => !v)}
aria-label="Навигация по курсу"
> >
{open ? "✕" : "☰ Уроки"} {open ? <X size={20} /> : <Menu size={20} />}
</button> </button>
{/* Overlay */}
{open && (
<div
className="lg:hidden fixed inset-0 bg-black/50 z-30"
onClick={() => setOpen(false)}
/>
)}
<aside <aside
className={`w-64 shrink-0 flex flex-col overflow-y-auto ${open ? "flex" : "hidden md:flex"}`} className={[
"w-64 flex flex-col overflow-y-auto",
"fixed top-[53px] left-0 bottom-0 z-40 transition-transform duration-200",
"lg:sticky lg:shrink-0 lg:translate-x-0",
open ? "translate-x-0" : "-translate-x-full lg:translate-x-0",
].join(" ")}
style={{ style={{
borderRight: "2px solid var(--border)", borderRight: "2px solid var(--border)",
backgroundColor: "var(--background)", backgroundColor: "var(--background)",
maxHeight: "calc(100vh - 53px)", maxHeight: "calc(100vh - 53px)",
position: "sticky",
top: "53px", top: "53px",
}} }}
> >
@@ -127,6 +146,7 @@ export function CourseSidebar({
<Link <Link
key={lesson.id} key={lesson.id}
href={`/courses/${course.slug}/lessons/${lesson.id}`} href={`/courses/${course.slug}/lessons/${lesson.id}`}
onClick={() => setOpen(false)}
className="flex items-center gap-2 px-4 py-2 text-sm leading-snug border-l-2 transition-colors" className="flex items-center gap-2 px-4 py-2 text-sm leading-snug border-l-2 transition-colors"
style={{ style={{
borderLeftColor: active ? "var(--foreground)" : "transparent", borderLeftColor: active ? "var(--foreground)" : "transparent",