From 843d2a8c7b97486660cc41f3addd5ac6bc7ddf12 Mon Sep 17 00:00:00 2001 From: Drew Galbraith Date: Mon, 22 Sep 2025 21:09:42 -0700 Subject: [PATCH] Create task view and form. --- frontend/app/components/TaskList.tsx | 292 +++++++++++++++++++++++++++ frontend/app/routes/home.test.tsx | 5 +- frontend/app/routes/home.tsx | 20 +- plan/01_CORE_MVP/frontend.md | 2 +- 4 files changed, 297 insertions(+), 22 deletions(-) create mode 100644 frontend/app/components/TaskList.tsx diff --git a/frontend/app/components/TaskList.tsx b/frontend/app/components/TaskList.tsx new file mode 100644 index 0000000..5bbb868 --- /dev/null +++ b/frontend/app/components/TaskList.tsx @@ -0,0 +1,292 @@ +import React, { useState, useMemo } from 'react' +import { + Box, + Typography, + FormControl, + InputLabel, + Select, + MenuItem, + Chip, + Stack, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + useMediaQuery, + useTheme, +} from '@mui/material' +import { useTasks } from '~/hooks/useTasks' +import { TaskStatus } from '~/types/task' +import LoadingSpinner from './LoadingSpinner' + +type SortOption = + | 'created_desc' + | 'created_asc' + | 'title_asc' + | 'title_desc' + | 'status' + +interface TaskListProps { + className?: string +} + +export function TaskList({ className }: TaskListProps) { + const { tasks, loading, error } = useTasks() + const [statusFilter, setStatusFilter] = useState('all') + const [sortBy, setSortBy] = useState('created_desc') + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down('md')) + + const filteredAndSortedTasks = useMemo(() => { + let filteredTasks = tasks + + // Apply status filter + if (statusFilter !== 'all') { + filteredTasks = tasks.filter(task => task.status === statusFilter) + } + + // Apply sorting + const sorted = [...filteredTasks].sort((a, b) => { + switch (sortBy) { + case 'created_desc': + return ( + new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ) + case 'created_asc': + return ( + new Date(a.created_at).getTime() - new Date(b.created_at).getTime() + ) + case 'title_asc': + return a.title.localeCompare(b.title) + case 'title_desc': + return b.title.localeCompare(a.title) + case 'status': { + const statusOrder = { + [TaskStatus.Todo]: 0, + [TaskStatus.Backlog]: 1, + [TaskStatus.Done]: 2, + } + return statusOrder[a.status] - statusOrder[b.status] + } + default: + return 0 + } + }) + + return sorted + }, [tasks, statusFilter, sortBy]) + + const getStatusColor = (status: TaskStatus) => { + switch (status) { + case TaskStatus.Todo: + return 'primary' + case TaskStatus.Done: + return 'success' + case TaskStatus.Backlog: + return 'default' + default: + return 'default' + } + } + + const formatCompactDate = (dateString: string) => { + const date = new Date(dateString) + const now = new Date() + const diffInDays = Math.floor( + (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24) + ) + + if (diffInDays === 0) return 'Today' + if (diffInDays === 1) return 'Yesterday' + if (diffInDays < 7) return `${diffInDays}d ago` + if (diffInDays < 30) return `${Math.floor(diffInDays / 7)}w ago` + if (diffInDays < 365) return `${Math.floor(diffInDays / 30)}mo ago` + return `${Math.floor(diffInDays / 365)}y ago` + } + + if (loading) { + return + } + + if (error) { + return ( + + + Error loading tasks + + + {error} + + + ) + } + + return ( + + {/* Filter and Sort Controls */} + + + + Status + + + + + Sort by + + + + + {filteredAndSortedTasks.length} task + {filteredAndSortedTasks.length !== 1 ? 's' : ''} + + + + + {/* Task Table */} + {filteredAndSortedTasks.length === 0 ? ( + + + No tasks found + + + {statusFilter === 'all' + ? 'Create your first task to get started!' + : `No ${statusFilter} tasks found.`} + + + ) : ( + + + + + Title + {!isMobile && ( + Description + )} + + Status + + + Created + + {!isMobile && ( + + Completed + + )} + + + + {filteredAndSortedTasks.map(task => ( + + + + + {task.title} + + {isMobile && task.description && ( + + {task.description} + + )} + + + {!isMobile && ( + + + {task.description || '—'} + + + )} + + + + + + {formatCompactDate(task.created_at)} + + + {!isMobile && ( + + + {task.completed_at + ? formatCompactDate(task.completed_at) + : '—'} + + + )} + + ))} + +
+
+ )} +
+ ) +} diff --git a/frontend/app/routes/home.test.tsx b/frontend/app/routes/home.test.tsx index 80dd150..d0d2427 100644 --- a/frontend/app/routes/home.test.tsx +++ b/frontend/app/routes/home.test.tsx @@ -9,8 +9,7 @@ describe('Home component', () => { expect( screen.getByText(/GTD-inspired task management system/i) ).toBeInTheDocument() - expect( - screen.getByText(/Task Management Interface Coming Soon/i) - ).toBeInTheDocument() + // TaskList component should be rendered (initially shows loading state) + expect(screen.getByText(/Loading.../i)).toBeInTheDocument() }) }) diff --git a/frontend/app/routes/home.tsx b/frontend/app/routes/home.tsx index b818ac6..1f0465c 100644 --- a/frontend/app/routes/home.tsx +++ b/frontend/app/routes/home.tsx @@ -1,5 +1,6 @@ import type { Route } from './+types/home' import { Box, Typography, Container } from '@mui/material' +import { TaskList } from '~/components/TaskList' export function meta(_: Route.MetaArgs) { return [ @@ -20,24 +21,7 @@ export default function Home() { what matters. - - - Task Management Interface Coming Soon - - - The task list, task cards, and quick capture components will be - implemented in the next phase. - - + ) diff --git a/plan/01_CORE_MVP/frontend.md b/plan/01_CORE_MVP/frontend.md index 325f4c7..adbca39 100644 --- a/plan/01_CORE_MVP/frontend.md +++ b/plan/01_CORE_MVP/frontend.md @@ -110,7 +110,7 @@ captains-log/ **UI Framework**: Use Material-UI (MUI) for consistent design system and components alongside CSS for custom styling. ### Task 3.1: Main App Component and Routing -- [ ] **Files**: `frontend/src/App.tsx`, `frontend/src/main.tsx` +- [x] **Files**: `frontend/src/App.tsx`, `frontend/src/main.tsx` - Setup React Router with basic navigation - Create main layout with MUI AppBar/Drawer and task area - Implement responsive design with MUI breakpoints and CSS