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,188 @@
|
||||
import { useState } from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Search, Filter, SortAsc } from 'lucide-react';
|
||||
import { Task } from './TaskCard';
|
||||
import TaskCard from './TaskCard';
|
||||
|
||||
interface TaskListProps {
|
||||
tasks: Task[];
|
||||
onTaskUpdate?: (taskId: string, updates: Partial<Task>) => void;
|
||||
onTaskEdit?: (task: Task) => void;
|
||||
}
|
||||
|
||||
type SortOption = 'dueDate' | 'priority' | 'title' | 'status';
|
||||
type FilterOption = 'all' | 'todo' | 'inProgress' | 'done' | 'overdue';
|
||||
|
||||
export default function TaskList({ tasks, onTaskUpdate, onTaskEdit }: TaskListProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [sortBy, setSortBy] = useState<SortOption>('dueDate');
|
||||
const [filterBy, setFilterBy] = useState<FilterOption>('all');
|
||||
|
||||
const isOverdue = (task: Task) => {
|
||||
if (!task.dueDate) return false;
|
||||
return task.dueDate < new Date() && task.status !== 'done';
|
||||
};
|
||||
|
||||
const filteredAndSortedTasks = tasks
|
||||
.filter(task => {
|
||||
// Search filter
|
||||
const matchesSearch = task.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
task.description?.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
|
||||
if (!matchesSearch) return false;
|
||||
|
||||
// Status filter
|
||||
switch (filterBy) {
|
||||
case 'overdue':
|
||||
return isOverdue(task);
|
||||
case 'all':
|
||||
return true;
|
||||
default:
|
||||
return task.status === filterBy;
|
||||
}
|
||||
})
|
||||
.sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case 'dueDate':
|
||||
if (!a.dueDate && !b.dueDate) return 0;
|
||||
if (!a.dueDate) return 1;
|
||||
if (!b.dueDate) return -1;
|
||||
return a.dueDate.getTime() - b.dueDate.getTime();
|
||||
|
||||
case 'priority':
|
||||
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
||||
return priorityOrder[b.priority] - priorityOrder[a.priority];
|
||||
|
||||
case 'title':
|
||||
return a.title.localeCompare(b.title);
|
||||
|
||||
case 'status':
|
||||
const statusOrder = { todo: 1, inProgress: 2, done: 3 };
|
||||
return statusOrder[a.status] - statusOrder[b.status];
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
const getFilterCount = (filter: FilterOption) => {
|
||||
switch (filter) {
|
||||
case 'overdue':
|
||||
return tasks.filter(isOverdue).length;
|
||||
case 'all':
|
||||
return tasks.length;
|
||||
default:
|
||||
return tasks.filter(task => task.status === filter).length;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold" data-testid="text-task-list-title">
|
||||
My Tasks
|
||||
</h2>
|
||||
<Badge variant="secondary" data-testid="badge-task-count">
|
||||
{filteredAndSortedTasks.length} tasks
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Search and Filters */}
|
||||
<div className="space-y-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search tasks..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
console.log('Search query:', e.target.value);
|
||||
}}
|
||||
className="pl-10"
|
||||
data-testid="input-search-tasks"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Select value={filterBy} onValueChange={(value: FilterOption) => {
|
||||
setFilterBy(value);
|
||||
console.log('Filter changed to:', value);
|
||||
}}>
|
||||
<SelectTrigger className="flex-1" data-testid="select-filter">
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="w-4 h-4" />
|
||||
<SelectValue />
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All ({getFilterCount('all')})</SelectItem>
|
||||
<SelectItem value="todo">To Do ({getFilterCount('todo')})</SelectItem>
|
||||
<SelectItem value="inProgress">In Progress ({getFilterCount('inProgress')})</SelectItem>
|
||||
<SelectItem value="done">Done ({getFilterCount('done')})</SelectItem>
|
||||
<SelectItem value="overdue">Overdue ({getFilterCount('overdue')})</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={sortBy} onValueChange={(value: SortOption) => {
|
||||
setSortBy(value);
|
||||
console.log('Sort changed to:', value);
|
||||
}}>
|
||||
<SelectTrigger className="flex-1" data-testid="select-sort">
|
||||
<div className="flex items-center gap-2">
|
||||
<SortAsc className="w-4 h-4" />
|
||||
<SelectValue />
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="dueDate">Due Date</SelectItem>
|
||||
<SelectItem value="priority">Priority</SelectItem>
|
||||
<SelectItem value="title">Title</SelectItem>
|
||||
<SelectItem value="status">Status</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task List */}
|
||||
<div className="space-y-3">
|
||||
{filteredAndSortedTasks.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-muted-foreground" data-testid="text-no-tasks">
|
||||
{searchQuery || filterBy !== 'all'
|
||||
? 'No tasks match your filters'
|
||||
: 'No tasks yet. Create your first task!'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredAndSortedTasks.map((task) => (
|
||||
<TaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
onPlay={() => {
|
||||
onTaskUpdate?.(task.id, { isTracking: true, timeTracked: task.timeTracked });
|
||||
console.log(`Timer started for ${task.title}`);
|
||||
}}
|
||||
onPause={() => {
|
||||
onTaskUpdate?.(task.id, { isTracking: false });
|
||||
console.log(`Timer paused for ${task.title}`);
|
||||
}}
|
||||
onEdit={() => {
|
||||
onTaskEdit?.(task);
|
||||
console.log(`Edit task ${task.title}`);
|
||||
}}
|
||||
onStatusChange={(newStatus) => {
|
||||
onTaskUpdate?.(task.id, { status: newStatus });
|
||||
console.log(`Task ${task.title} status changed to ${newStatus}`);
|
||||
}}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user