Add kanban board view, project creation, and task assignment
This commit is contained in:
147
src/pages/Project.tsx
Normal file
147
src/pages/Project.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { LayoutList, LayoutGrid, Plus } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTasksStore } from '@/stores/tasks';
|
||||
import { TaskItem } from '@/components/TaskItem';
|
||||
import { AddTask } from '@/components/AddTask';
|
||||
import { BoardView } from '@/pages/Board';
|
||||
import { api } from '@/lib/api';
|
||||
import type { Project as ProjectType, Section } from '@/types';
|
||||
|
||||
export function ProjectPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { tasks, isLoading, fetchTasks, setSelectedTask } = useTasksStore();
|
||||
const [project, setProject] = useState<ProjectType | null>(null);
|
||||
const [sections, setSections] = useState<Section[]>([]);
|
||||
|
||||
const isBoardView = location.pathname.endsWith('/board');
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
fetchTasks({ projectId: id, completed: false });
|
||||
api.getProject(id).then((p) => {
|
||||
setProject(p);
|
||||
setSections(p.sections || []);
|
||||
}).catch(console.error);
|
||||
}, [id]);
|
||||
|
||||
if (!project) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12 text-gray-500">
|
||||
Loading project...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const toggleView = () => {
|
||||
if (isBoardView) {
|
||||
navigate(`/project/${id}`);
|
||||
} else {
|
||||
navigate(`/project/${id}/board`);
|
||||
}
|
||||
};
|
||||
|
||||
// Group tasks by section
|
||||
const unsectionedTasks = tasks.filter((t) => !t.sectionId);
|
||||
const tasksBySection = sections.map((section) => ({
|
||||
section,
|
||||
tasks: tasks.filter((t) => t.sectionId === section.id),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className="w-4 h-4 rounded"
|
||||
style={{ backgroundColor: project.color }}
|
||||
/>
|
||||
<h1 className="text-2xl font-bold text-gray-900">{project.name}</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 bg-gray-100 rounded-lg p-0.5">
|
||||
<button
|
||||
onClick={() => !isBoardView || toggleView()}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md transition-colors',
|
||||
!isBoardView
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
)}
|
||||
>
|
||||
<LayoutList className="w-4 h-4" />
|
||||
List
|
||||
</button>
|
||||
<button
|
||||
onClick={() => isBoardView || toggleView()}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md transition-colors',
|
||||
isBoardView
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
)}
|
||||
>
|
||||
<LayoutGrid className="w-4 h-4" />
|
||||
Board
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isBoardView ? (
|
||||
<BoardView
|
||||
project={project}
|
||||
sections={sections}
|
||||
tasks={tasks}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
) : (
|
||||
/* List view */
|
||||
<div className="space-y-6">
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12 text-gray-500">Loading tasks...</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Unsectioned tasks */}
|
||||
{unsectionedTasks.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
{unsectionedTasks.map((task) => (
|
||||
<TaskItem
|
||||
key={task.id}
|
||||
task={task}
|
||||
onClick={() => setSelectedTask(task)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add task for unsectioned */}
|
||||
<AddTask projectId={id} />
|
||||
|
||||
{/* Sections */}
|
||||
{tasksBySection.map(({ section, tasks: sectionTasks }) => (
|
||||
<div key={section.id}>
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-2 px-3 py-1 border-b border-gray-200">
|
||||
{section.name}
|
||||
</h3>
|
||||
<div className="space-y-1">
|
||||
{sectionTasks.map((task) => (
|
||||
<TaskItem
|
||||
key={task.id}
|
||||
task={task}
|
||||
onClick={() => setSelectedTask(task)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<AddTask projectId={id} sectionId={section.id} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user