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
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user