Add src/components/Sidebar.tsx
This commit is contained in:
221
src/components/Sidebar.tsx
Normal file
221
src/components/Sidebar.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
'use client';
|
||||
|
||||
import { useApp } from './AppProvider';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function Sidebar() {
|
||||
const {
|
||||
view, setView, selectedProject, setSelectedProject,
|
||||
sidebarOpen, setSidebarOpen, showNewProjectModal, setShowNewProjectModal,
|
||||
showNewTaskModal, setShowNewTaskModal, projects, setProjects, refreshTasks, refreshProjects,
|
||||
} = useApp();
|
||||
|
||||
const [projectName, setProjectName] = useState('');
|
||||
const [projectDescription, setProjectDescription] = useState('');
|
||||
const [projectColor, setProjectColor] = useState('#3b82f6');
|
||||
const [creatingProject, setCreatingProject] = useState(false);
|
||||
|
||||
const views: { key: typeof view; label: string; icon: string }[] = [
|
||||
{ key: 'list', label: 'List View', icon: '☰' },
|
||||
{ key: 'kanban', label: 'Kanban Board', icon: '▦' },
|
||||
{ key: 'calendar', label: 'Calendar', icon: '📅' },
|
||||
];
|
||||
|
||||
const handleNewProject = async () => {
|
||||
if (!projectName.trim()) return;
|
||||
try {
|
||||
await fetch('/api/projects', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: projectName.trim(),
|
||||
description: projectDescription.trim(),
|
||||
color: projectColor,
|
||||
}),
|
||||
});
|
||||
setProjectName('');
|
||||
setProjectDescription('');
|
||||
setProjectColor('#3b82f6');
|
||||
setCreatingProject(false);
|
||||
refreshProjects();
|
||||
} catch (error) {
|
||||
console.error('Failed to create project:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteProject = async (id: string) => {
|
||||
if (!confirm('Delete this project and all its tasks?')) return;
|
||||
try {
|
||||
await fetch(`/api/projects/${id}`, { method: 'DELETE' });
|
||||
if (selectedProject === id) setSelectedProject(null);
|
||||
refreshProjects();
|
||||
refreshTasks();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete project:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col h-full bg-sidebar text-white transition-all duration-300 ${sidebarOpen ? 'w-64' : 'w-16'}`}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
||||
{sidebarOpen && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl font-bold">VixTix</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
className="p-1.5 rounded-lg hover:bg-sidebar-hover transition-colors"
|
||||
>
|
||||
<span className="text-lg">{sidebarOpen ? '◀' : '▶'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* New Task Button */}
|
||||
<div className="p-3">
|
||||
<button
|
||||
onClick={() => setShowNewTaskModal(true)}
|
||||
className="w-full flex items-center justify-center gap-2 bg-accent hover:bg-accent-hover text-white py-2.5 px-4 rounded-lg transition-colors font-medium"
|
||||
>
|
||||
<span className="text-lg">+</span>
|
||||
{sidebarOpen && <span>New Task</span>}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Views */}
|
||||
{sidebarOpen && (
|
||||
<div className="px-3 mb-4">
|
||||
<p className="text-xs text-gray-400 uppercase tracking-wider mb-2 px-2">Views</p>
|
||||
<div className="space-y-1">
|
||||
{views.map((v) => (
|
||||
<button
|
||||
key={v.key}
|
||||
onClick={() => setView(v.key)}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors text-sm ${
|
||||
view === v.key
|
||||
? 'bg-sidebar-active text-white'
|
||||
: 'text-gray-300 hover:bg-sidebar-hover hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span className="text-base">{v.icon}</span>
|
||||
{v.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Projects */}
|
||||
<div className="flex-1 overflow-y-auto px-3">
|
||||
{sidebarOpen && (
|
||||
<div className="flex items-center justify-between mb-2 px-2">
|
||||
<p className="text-xs text-gray-400 uppercase tracking-wider">Projects</p>
|
||||
<button
|
||||
onClick={() => setCreatingProject(!creatingProject)}
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
title="New Project"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* New project form */}
|
||||
{creatingProject && sidebarOpen && (
|
||||
<div className="mb-3 p-3 bg-sidebar-hover rounded-lg animate-slide-in">
|
||||
<input
|
||||
type="text"
|
||||
value={projectName}
|
||||
onChange={(e) => setProjectName(e.target.value)}
|
||||
placeholder="Project name"
|
||||
className="w-full bg-transparent border border-white/20 rounded px-2 py-1.5 text-sm text-white placeholder-gray-500 mb-2 focus:outline-none focus:border-accent"
|
||||
autoFocus
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={projectDescription}
|
||||
onChange={(e) => setProjectDescription(e.target.value)}
|
||||
placeholder="Description (optional)"
|
||||
className="w-full bg-transparent border border-white/20 rounded px-2 py-1.5 text-sm text-white placeholder-gray-500 mb-2 focus:outline-none focus:border-accent"
|
||||
/>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs text-gray-400">Color:</span>
|
||||
<input
|
||||
type="color"
|
||||
value={projectColor}
|
||||
onChange={(e) => setProjectColor(e.target.value)}
|
||||
className="w-8 h-8 rounded cursor-pointer border-0 bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleNewProject}
|
||||
className="flex-1 bg-accent hover:bg-accent-hover text-white py-1.5 rounded text-sm transition-colors"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setCreatingProject(false); setProjectName(''); }}
|
||||
className="flex-1 bg-gray-600 hover:bg-gray-500 text-white py-1.5 rounded text-sm transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Project list */}
|
||||
<div className="space-y-0.5">
|
||||
{/* All Projects option */}
|
||||
<button
|
||||
onClick={() => setSelectedProject(null)}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors text-sm ${
|
||||
!selectedProject
|
||||
? 'bg-sidebar-active text-white'
|
||||
: 'text-gray-300 hover:bg-sidebar-hover hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span className="text-base">📋</span>
|
||||
{sidebarOpen && <span className="truncate">All Projects</span>}
|
||||
</button>
|
||||
|
||||
{projects.map((project) => (
|
||||
<div
|
||||
key={project.id}
|
||||
className={`group flex items-center gap-3 px-3 py-2 rounded-lg transition-colors text-sm ${
|
||||
selectedProject === project.id
|
||||
? 'bg-sidebar-active text-white'
|
||||
: 'text-gray-300 hover:bg-sidebar-hover hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className="w-3 h-3 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: project.color }}
|
||||
/>
|
||||
{sidebarOpen && (
|
||||
<>
|
||||
<span className="truncate flex-1 text-left">{project.name}</span>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteProject(project.id); }}
|
||||
className="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-400 transition-all"
|
||||
title="Delete project"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
{sidebarOpen && (
|
||||
<div className="p-3 border-t border-white/10 text-xs text-gray-500 text-center">
|
||||
VixTix v1.0
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user