143 lines
4.8 KiB
TypeScript
143 lines
4.8 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { curriculum } from "@/lib/curriculum";
|
|
import { cn } from "@/lib/utils";
|
|
import { useState } from "react";
|
|
|
|
const unitColorMap = {
|
|
"unit-1": {
|
|
active: "bg-unit-1-light text-unit-1-dark border-unit-1/20",
|
|
dot: "bg-unit-1",
|
|
heading: "text-unit-1-dark",
|
|
hoverBg: "hover:bg-unit-1-light/50",
|
|
},
|
|
"unit-2": {
|
|
active: "bg-unit-2-light text-unit-2-dark border-unit-2/20",
|
|
dot: "bg-unit-2",
|
|
heading: "text-unit-2-dark",
|
|
hoverBg: "hover:bg-unit-2-light/50",
|
|
},
|
|
"unit-3": {
|
|
active: "bg-unit-3-light text-unit-3-dark border-unit-3/20",
|
|
dot: "bg-unit-3",
|
|
heading: "text-unit-3-dark",
|
|
hoverBg: "hover:bg-unit-3-light/50",
|
|
},
|
|
"unit-4": {
|
|
active: "bg-unit-4-light text-unit-4-dark border-unit-4/20",
|
|
dot: "bg-unit-4",
|
|
heading: "text-unit-4-dark",
|
|
hoverBg: "hover:bg-unit-4-light/50",
|
|
},
|
|
};
|
|
|
|
export function Sidebar() {
|
|
const pathname = usePathname();
|
|
const [openUnits, setOpenUnits] = useState<Set<number>>(() => {
|
|
const open = new Set<number>();
|
|
for (const unit of curriculum) {
|
|
if (pathname.includes(unit.slug)) {
|
|
open.add(unit.number);
|
|
}
|
|
}
|
|
if (open.size === 0) open.add(1);
|
|
return open;
|
|
});
|
|
|
|
function toggleUnit(num: number) {
|
|
setOpenUnits((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(num)) next.delete(num);
|
|
else next.add(num);
|
|
return next;
|
|
});
|
|
}
|
|
|
|
return (
|
|
<aside className="hidden w-64 shrink-0 border-r border-border/60 bg-surface lg:block">
|
|
<nav className="sticky top-[4.125rem] h-[calc(100vh-4.125rem)] overflow-y-auto p-4">
|
|
<Link
|
|
href="/lessons"
|
|
className={cn(
|
|
"mb-4 flex items-center gap-2 rounded-xl px-3 py-2 text-sm font-medium transition-all duration-200",
|
|
pathname === "/lessons"
|
|
? "bg-foreground text-background shadow-[var(--shadow-sm)]"
|
|
: "text-muted hover:bg-background hover:text-foreground",
|
|
)}
|
|
>
|
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
|
</svg>
|
|
All Topics
|
|
</Link>
|
|
|
|
<div className="mb-3 h-px bg-border/60" />
|
|
|
|
{curriculum.map((unit) => {
|
|
const colors = unitColorMap[unit.color];
|
|
const isOpen = openUnits.has(unit.number);
|
|
|
|
return (
|
|
<div key={unit.slug} className="mb-1">
|
|
<button
|
|
onClick={() => toggleUnit(unit.number)}
|
|
className={cn(
|
|
"flex w-full items-center justify-between rounded-xl px-3 py-2 text-left text-sm font-semibold transition-all duration-200",
|
|
colors.heading,
|
|
colors.hoverBg,
|
|
)}
|
|
>
|
|
<span className="truncate">Unit {unit.number}: {unit.title}</span>
|
|
<svg
|
|
className={cn(
|
|
"h-4 w-4 shrink-0 transition-transform duration-200",
|
|
isOpen && "rotate-90",
|
|
)}
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
strokeWidth={2}
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</button>
|
|
|
|
{isOpen && (
|
|
<div className="ml-2 mt-0.5 space-y-0.5 border-l-2 border-border/40 pl-2">
|
|
{unit.topics.map((topic) => {
|
|
const href = `/lessons/${unit.slug}/${topic.slug}`;
|
|
const isActive = pathname === href;
|
|
|
|
return (
|
|
<Link
|
|
key={topic.slug}
|
|
href={href}
|
|
className={cn(
|
|
"flex items-center gap-2 rounded-lg px-2.5 py-1.5 text-sm transition-all duration-200",
|
|
isActive
|
|
? cn(colors.active, "font-medium shadow-[var(--shadow-sm)]")
|
|
: "text-muted hover:text-foreground",
|
|
)}
|
|
>
|
|
<span
|
|
className={cn(
|
|
"h-1.5 w-1.5 shrink-0 rounded-full transition-colors",
|
|
isActive ? colors.dot : "bg-border",
|
|
)}
|
|
/>
|
|
<span className="truncate">{topic.shortTitle}</span>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</nav>
|
|
</aside>
|
|
);
|
|
}
|