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