Files
bizzleandClaude b7416cc618 feat: UI revamp with sidebar nav and grouped library
Replace top Nav with Sidebar, add lib/group.js for library grouping,
tokenized color/alert styles, and tighter Settings/Canvas export layout.
Adds @tabler/icons-react. Verified production build (standalone) passes,
so the Docker image built from this tree matches the local app.

Co-authored-by: Claude <claude-code@anthropic.com>
2026-06-25 18:33:08 -04:00

125 lines
4.8 KiB
React

"use client";
// components/Sidebar.jsx — the Open Workspace shell: brand, primary nav, a live
// library folder tree grouped by subject, and a pinned theme toggle + New button.
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
IconPencil, IconPlus, IconSettings, IconMoon, IconSun,
IconChevronDown, IconChevronRight, IconFileText, IconPencilPlus,
} from "@tabler/icons-react";
import { groupBySubject } from "@/lib/group";
const NAV = [
{ href: "/", label: "Create", icon: IconPlus },
{ href: "/settings", label: "Settings", icon: IconSettings },
];
export default function Sidebar() {
const pathname = usePathname();
const [theme, setTheme] = useState(null);
const [items, setItems] = useState([]);
const [collapsed, setCollapsed] = useState({}); // subject -> true when collapsed
useEffect(() => {
setTheme(document.documentElement.dataset.theme === "dark" ? "dark" : "light");
}, []);
const load = useCallback(() => {
fetch("/api/assignments")
.then((r) => r.json())
.then((d) => setItems(d.assignments || []))
.catch(() => {});
}, []);
// Reload the tree whenever the route changes (covers create / delete / rename).
useEffect(() => { load(); }, [load, pathname]);
function toggleTheme() {
const next = document.documentElement.dataset.theme === "dark" ? "light" : "dark";
if (next === "dark") document.documentElement.dataset.theme = "dark";
else delete document.documentElement.dataset.theme;
try { localStorage.setItem("theme", next); } catch {}
setTheme(next);
}
const groups = useMemo(() => groupBySubject(items), [items]);
const activeId = pathname.startsWith("/editor/") ? decodeURIComponent(pathname.split("/editor/")[1] || "") : "";
const isNavActive = (href) => (href === "/" ? pathname === "/" : pathname.startsWith(href));
return (
<aside className="sidebar" aria-label="Sidebar">
<Link href="/" className="sidebar-brand">
<span className="brand-mark" aria-hidden="true"><IconPencil size={19} stroke={2} /></span>
<span>
<span className="brand-name" style={{ display: "block" }}>Mr.&nbsp;Drew&rsquo;s</span>
<span className="brand-sub">Assignment Creator</span>
</span>
</Link>
<nav className="sidebar-nav" aria-label="Main">
{NAV.map(({ href, label, icon: Icon }) => (
<Link key={href} href={href} className={`navrow${isNavActive(href) ? " active" : ""}`}>
<Icon size={19} stroke={2} />
<span className="navrow-label">{label}</span>
</Link>
))}
</nav>
<div className="sidebar-scroll">
<div className="sidebar-label">
<span>Library</span>
<Link href="/library" className="navrow-label" style={{ fontSize: "0.66rem", color: "var(--board-deep)", fontWeight: 700 }}>
All
</Link>
</div>
{groups.length === 0 && (
<p className="faint" style={{ fontSize: "0.8rem", padding: "4px 10px", margin: 0 }}>
No assignments yet.
</p>
)}
{groups.map((g) => {
const open = !collapsed[g.key];
return (
<div key={g.key} className="tree-group">
<button
className="tree-folder"
onClick={() => setCollapsed((c) => ({ ...c, [g.key]: open }))}
aria-expanded={open}
>
{open ? <IconChevronDown size={15} className="chev" /> : <IconChevronRight size={15} className="chev" />}
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{g.key}</span>
<span className="tree-count">{g.items.length}</span>
</button>
{open && g.items.map((a) => (
<Link key={a.id} href={"/editor/" + a.id} className={`tree-leaf${activeId === a.id ? " active" : ""}`} title={a.title}>
<IconFileText size={15} style={{ flex: "none" }} />
<span>{a.title}</span>
</Link>
))}
</div>
);
})}
</div>
<div className="sidebar-foot">
<button
type="button"
className="icon-btn theme-toggle"
onClick={toggleTheme}
aria-label={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
title={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
>
{theme === "dark" ? <IconSun size={18} /> : <IconMoon size={18} />}
</button>
<Link href="/" className="btn btn-primary btn-block">
<IconPencilPlus size={17} /> <span className="navrow-label">New assignment</span>
</Link>
</div>
</aside>
);
}