Files
todo-app/apps/web/src/components/Sidebar.tsx
Hammer 98ea0427bb Initial todo app setup
- Backend: Bun + Elysia + Drizzle ORM + PostgreSQL
- Frontend: React + Vite + TailwindCSS + Zustand
- Auth: better-auth with invite-only system
- Features: Tasks, Projects, Sections, Labels, Comments
- Hammer API: Dedicated endpoints for AI assistant integration
- Unit tests: 24 passing tests
- Docker: Compose file for deployment
2026-01-28 14:02:15 +00:00

186 lines
6.7 KiB
TypeScript

import { useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import {
Inbox, Calendar, CalendarDays, Plus, ChevronDown, ChevronRight,
Hash, Settings, LogOut, User, FolderPlus, Tag
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { useAuthStore } from '@/stores/auth';
import { useTasksStore } from '@/stores/tasks';
export function Sidebar() {
const location = useLocation();
const navigate = useNavigate();
const { user, logout } = useAuthStore();
const { projects, labels } = useTasksStore();
const [projectsExpanded, setProjectsExpanded] = useState(true);
const [labelsExpanded, setLabelsExpanded] = useState(true);
const handleLogout = async () => {
await logout();
navigate('/login');
};
const inbox = projects.find(p => p.isInbox);
const regularProjects = projects.filter(p => !p.isInbox);
const navItems = [
{ path: '/inbox', icon: Inbox, label: 'Inbox', color: '#3b82f6' },
{ path: '/today', icon: Calendar, label: 'Today', color: '#22c55e' },
{ path: '/upcoming', icon: CalendarDays, label: 'Upcoming', color: '#8b5cf6' },
];
return (
<aside className="w-64 h-screen bg-gray-50 border-r border-gray-200 flex flex-col">
{/* User section */}
<div className="p-4 border-b border-gray-200">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white text-sm font-medium">
{user?.name?.charAt(0).toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">{user?.name}</p>
<p className="text-xs text-gray-500 truncate">{user?.email}</p>
</div>
</div>
</div>
{/* Navigation */}
<nav className="flex-1 overflow-y-auto p-2">
{/* Main nav items */}
<div className="space-y-1">
{navItems.map((item) => (
<Link
key={item.path}
to={item.path}
className={cn(
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors',
location.pathname === item.path
? 'bg-blue-50 text-blue-600'
: 'text-gray-700 hover:bg-gray-100'
)}
>
<item.icon className="w-4 h-4" style={{ color: item.color }} />
<span>{item.label}</span>
</Link>
))}
</div>
{/* Projects section */}
<div className="mt-6">
<button
onClick={() => setProjectsExpanded(!projectsExpanded)}
className="w-full flex items-center justify-between px-3 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider hover:text-gray-700"
>
<span>Projects</span>
<div className="flex items-center gap-1">
<Link
to="/projects/new"
className="p-1 hover:bg-gray-200 rounded"
onClick={(e) => e.stopPropagation()}
>
<Plus className="w-3 h-3" />
</Link>
{projectsExpanded ? (
<ChevronDown className="w-3 h-3" />
) : (
<ChevronRight className="w-3 h-3" />
)}
</div>
</button>
{projectsExpanded && (
<div className="mt-1 space-y-1">
{regularProjects.map((project) => (
<Link
key={project.id}
to={`/project/${project.id}`}
className={cn(
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors',
location.pathname === `/project/${project.id}`
? 'bg-blue-50 text-blue-600'
: 'text-gray-700 hover:bg-gray-100'
)}
>
<span
className="w-3 h-3 rounded"
style={{ backgroundColor: project.color }}
/>
<span className="truncate">{project.name}</span>
</Link>
))}
</div>
)}
</div>
{/* Labels section */}
<div className="mt-6">
<button
onClick={() => setLabelsExpanded(!labelsExpanded)}
className="w-full flex items-center justify-between px-3 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider hover:text-gray-700"
>
<span>Labels</span>
<div className="flex items-center gap-1">
<Link
to="/labels/new"
className="p-1 hover:bg-gray-200 rounded"
onClick={(e) => e.stopPropagation()}
>
<Plus className="w-3 h-3" />
</Link>
{labelsExpanded ? (
<ChevronDown className="w-3 h-3" />
) : (
<ChevronRight className="w-3 h-3" />
)}
</div>
</button>
{labelsExpanded && labels.length > 0 && (
<div className="mt-1 space-y-1">
{labels.map((label) => (
<Link
key={label.id}
to={`/label/${label.id}`}
className={cn(
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors',
location.pathname === `/label/${label.id}`
? 'bg-blue-50 text-blue-600'
: 'text-gray-700 hover:bg-gray-100'
)}
>
<Tag className="w-3 h-3" style={{ color: label.color }} />
<span className="truncate">{label.name}</span>
{label.taskCount !== undefined && label.taskCount > 0 && (
<span className="ml-auto text-xs text-gray-400">{label.taskCount}</span>
)}
</Link>
))}
</div>
)}
</div>
</nav>
{/* Bottom section */}
<div className="p-2 border-t border-gray-200">
{user?.role === 'admin' && (
<Link
to="/admin"
className="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-gray-700 hover:bg-gray-100"
>
<Settings className="w-4 h-4" />
<span>Admin</span>
</Link>
)}
<button
onClick={handleLogout}
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-gray-700 hover:bg-gray-100"
>
<LogOut className="w-4 h-4" />
<span>Sign out</span>
</button>
</div>
</aside>
);
}