28ad3f1535
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
208 lines
7.5 KiB
TypeScript
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>
|
|
);
|
|
} |