Files
task-manager/client/src/components/KanbanBoard.tsx
T
paul-nothaft 28ad3f1535 Add core components for task management and navigation
This commit introduces the foundational UI components and logic for the task management application, including task creation, calendar views, Kanban boards, and navigation elements.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/yy9YLEW
2025-09-11 08:59:20 +00:00

208 lines
7.5 KiB
TypeScript

import { useState } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { LayoutGrid, Calendar as CalendarIcon, ChevronLeft, ChevronRight } from 'lucide-react';
import { Task } from './TaskCard';
import TaskCard from './TaskCard';
import { format, startOfWeek, endOfWeek, startOfMonth, endOfMonth, addWeeks, addMonths, isWithinInterval } from 'date-fns';
interface KanbanBoardProps {
tasks: Task[];
onTaskStatusChange?: (taskId: string, newStatus: Task['status']) => void;
onTaskUpdate?: (taskId: string, updates: Partial<Task>) => void;
}
type ViewMode = 'traditional' | 'weekly' | 'monthly';
export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate }: KanbanBoardProps) {
const [viewMode, setViewMode] = useState<ViewMode>('traditional');
const [currentDate, setCurrentDate] = useState(new Date());
const [draggedTask, setDraggedTask] = useState<string | null>(null);
const columns = [
{ id: 'todo', title: 'To Do', status: 'todo' as const },
{ id: 'inProgress', title: 'In Progress', status: 'inProgress' as const },
{ id: 'done', title: 'Done', status: 'done' as const }
];
const getTasksForColumn = (status: Task['status']) => {
let filteredTasks = tasks.filter(task => task.status === status);
if (viewMode === 'weekly') {
const weekStart = startOfWeek(currentDate);
const weekEnd = endOfWeek(currentDate);
filteredTasks = filteredTasks.filter(task =>
task.dueDate && isWithinInterval(task.dueDate, { start: weekStart, end: weekEnd })
);
} else if (viewMode === 'monthly') {
const monthStart = startOfMonth(currentDate);
const monthEnd = endOfMonth(currentDate);
filteredTasks = filteredTasks.filter(task =>
task.dueDate && isWithinInterval(task.dueDate, { start: monthStart, end: monthEnd })
);
}
return filteredTasks;
};
const handleDragStart = (taskId: string) => {
setDraggedTask(taskId);
};
const handleDragEnd = () => {
setDraggedTask(null);
};
const handleDrop = (status: Task['status']) => {
if (draggedTask) {
onTaskStatusChange?.(draggedTask, status);
console.log(`Task ${draggedTask} moved to ${status}`);
setDraggedTask(null);
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
const navigatePeriod = (direction: 'prev' | 'next') => {
if (viewMode === 'weekly') {
setCurrentDate(addWeeks(currentDate, direction === 'next' ? 1 : -1));
} else if (viewMode === 'monthly') {
setCurrentDate(addMonths(currentDate, direction === 'next' ? 1 : -1));
}
};
const getPeriodTitle = () => {
if (viewMode === 'weekly') {
return `Week of ${format(startOfWeek(currentDate), 'MMM d')}`;
} else if (viewMode === 'monthly') {
return format(currentDate, 'MMMM yyyy');
}
return 'All Tasks';
};
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<LayoutGrid className="w-5 h-5 text-primary" />
<h2 className="text-lg font-semibold" data-testid="text-kanban-title">
Task Board
</h2>
</div>
{viewMode !== 'traditional' && (
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
onClick={() => navigatePeriod('prev')}
data-testid="button-prev-period"
className="w-8 h-8"
>
<ChevronLeft className="w-4 h-4" />
</Button>
<span className="text-sm font-medium min-w-[120px] text-center">
{getPeriodTitle()}
</span>
<Button
variant="outline"
size="icon"
onClick={() => navigatePeriod('next')}
data-testid="button-next-period"
className="w-8 h-8"
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
)}
</div>
{/* View Mode Tabs */}
<Tabs value={viewMode} onValueChange={(value) => setViewMode(value as ViewMode)}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="traditional" data-testid="tab-traditional">
<LayoutGrid className="w-4 h-4 mr-2" />
Traditional
</TabsTrigger>
<TabsTrigger value="weekly" data-testid="tab-weekly">
<CalendarIcon className="w-4 h-4 mr-2" />
Weekly
</TabsTrigger>
<TabsTrigger value="monthly" data-testid="tab-monthly">
<CalendarIcon className="w-4 h-4 mr-2" />
Monthly
</TabsTrigger>
</TabsList>
<TabsContent value={viewMode} className="mt-4">
{/* Kanban Columns */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{columns.map((column) => {
const columnTasks = getTasksForColumn(column.status);
return (
<Card
key={column.id}
className="p-4"
onDragOver={handleDragOver}
onDrop={() => handleDrop(column.status)}
data-testid={`column-${column.id}`}
>
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-sm" data-testid={`text-column-title-${column.id}`}>
{column.title}
</h3>
<Badge variant="secondary" className="text-xs">
{columnTasks.length}
</Badge>
</div>
<div className="space-y-3 min-h-[200px]">
{columnTasks.map((task) => (
<div
key={task.id}
draggable
onDragStart={() => handleDragStart(task.id)}
onDragEnd={handleDragEnd}
className={`cursor-move ${draggedTask === task.id ? 'opacity-50' : ''}`}
>
<TaskCard
task={task}
onPlay={() => {
onTaskUpdate?.(task.id, { isTracking: true });
console.log(`Timer started for ${task.title}`);
}}
onPause={() => {
onTaskUpdate?.(task.id, { isTracking: false });
console.log(`Timer paused for ${task.title}`);
}}
onEdit={() => {
console.log(`Edit task ${task.title}`);
}}
isDragging={draggedTask === task.id}
/>
</div>
))}
{columnTasks.length === 0 && (
<div className="text-center text-muted-foreground text-sm py-8">
No tasks in {column.title.toLowerCase()}
</div>
)}
</div>
</Card>
);
})}
</div>
</TabsContent>
</Tabs>
</div>
);
}