113 lines
3.5 KiB
TypeScript
113 lines
3.5 KiB
TypeScript
|
||
import { useState } from "react";
|
||
import { Link } from "react-router-dom";
|
||
import type { Project } from "@/lib/api";
|
||
import { logout } from "@/lib/auth-client";
|
||
|
||
interface Props {
|
||
projects: Project[];
|
||
activeId?: string;
|
||
}
|
||
|
||
export default function Sidebar({ projects, activeId }: Props) {
|
||
const [open, setOpen] = useState(false);
|
||
|
||
const nav = (
|
||
<>
|
||
<div className="p-4 border-b border-[var(--border)] flex items-center justify-between">
|
||
<Link to="/" className="text-lg font-bold tracking-tight">
|
||
Team Board
|
||
</Link>
|
||
{/* Close button on mobile */}
|
||
<button
|
||
className="md:hidden text-[var(--muted)] hover:text-[var(--fg)] text-xl"
|
||
onClick={() => setOpen(false)}
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
<nav className="flex-1 overflow-y-auto p-2">
|
||
<Link
|
||
to="/projects/new"
|
||
onClick={() => setOpen(false)}
|
||
className="flex items-center gap-2 px-3 py-2 mb-2 rounded text-sm text-[var(--accent)]
|
||
hover:bg-[var(--accent)]/10 transition-colors"
|
||
>
|
||
<span className="text-lg leading-none">+</span>
|
||
Новый проект
|
||
</Link>
|
||
|
||
<div className="text-xs uppercase text-[var(--muted)] px-2 py-1 mb-1">Проекты</div>
|
||
{projects.map((p) => (
|
||
<Link
|
||
key={p.id}
|
||
to={`/projects/${p.id}`}
|
||
onClick={() => setOpen(false)}
|
||
className={`block px-3 py-2 rounded text-sm transition-colors ${
|
||
activeId === p.id
|
||
? "bg-[var(--accent)]/10 text-[var(--accent)]"
|
||
: "hover:bg-white/5 text-[var(--fg)]"
|
||
}`}
|
||
>
|
||
{p.name}
|
||
</Link>
|
||
))}
|
||
</nav>
|
||
|
||
<div className="p-3 border-t border-[var(--border)] flex items-center justify-between">
|
||
<span className="text-xs text-[var(--muted)]">Team Board v0.2</span>
|
||
<div className="flex items-center gap-2">
|
||
<Link
|
||
to="/settings"
|
||
onClick={() => setOpen(false)}
|
||
className="text-[var(--muted)] hover:text-[var(--fg)] transition-colors cursor-pointer"
|
||
title="Настройки"
|
||
>
|
||
⚙️
|
||
</Link>
|
||
<button
|
||
onClick={logout}
|
||
className="text-[var(--muted)] hover:text-[var(--fg)] transition-colors cursor-pointer"
|
||
title="Выйти"
|
||
>
|
||
🚪
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
|
||
return (
|
||
<>
|
||
{/* Mobile hamburger */}
|
||
<button
|
||
className="md:hidden fixed top-3 left-3 z-40 p-2 bg-[var(--card)] border border-[var(--border)]
|
||
rounded-lg text-[var(--fg)]"
|
||
onClick={() => setOpen(true)}
|
||
>
|
||
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
||
<path d="M2 4.5h16M2 10h16M2 15.5h16" stroke="currentColor" strokeWidth="1.5" fill="none" />
|
||
</svg>
|
||
</button>
|
||
|
||
{/* Mobile overlay */}
|
||
{open && (
|
||
<div className="md:hidden fixed inset-0 bg-black/60 z-40" onClick={() => setOpen(false)} />
|
||
)}
|
||
|
||
{/* Sidebar: always visible on desktop, slide-in on mobile */}
|
||
<aside
|
||
className={`
|
||
fixed md:static z-50 top-0 left-0 h-[100dvh] w-60 shrink-0
|
||
border-r border-[var(--border)] bg-[var(--card)] flex flex-col
|
||
transition-transform duration-200
|
||
${open ? "translate-x-0" : "-translate-x-full md:translate-x-0"}
|
||
`}
|
||
>
|
||
{nav}
|
||
</aside>
|
||
</>
|
||
);
|
||
}
|