diff --git a/frontend/app/components/TaskList.tsx b/frontend/app/components/TaskList.tsx deleted file mode 100644 index 8503ffb..0000000 --- a/frontend/app/components/TaskList.tsx +++ /dev/null @@ -1,432 +0,0 @@ -import React, { useState, useMemo } from 'react' -import { - Box, - Typography, - FormControl, - InputLabel, - Select, - MenuItem, - Chip, - Stack, - Paper, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - useMediaQuery, - useTheme, - Checkbox, - Toolbar, - Button, - Dialog, - DialogTitle, - DialogContent, - DialogContentText, - DialogActions, -} from '@mui/material' -import { Delete as DeleteIcon } from '@mui/icons-material' -import { useTasks } from '~/hooks/useTasks' -import { TaskStatus, type Task } from '~/types/task' -import LoadingSpinner from './LoadingSpinner' - -type SortOption = - | 'created_desc' - | 'created_asc' - | 'title_asc' - | 'title_desc' - | 'status' - -interface TaskListProps { - className?: string - initialTasks?: Task[] -} - -export function TaskList({ className, initialTasks }: TaskListProps) { - const { tasks, loading, error, deleteTask } = useTasks({ - autoFetch: !initialTasks, - initialData: initialTasks, - }) - const [statusFilter, setStatusFilter] = useState('all') - const [sortBy, setSortBy] = useState('created_desc') - const [selectedTaskIds, setSelectedTaskIds] = useState>(new Set()) - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) - 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` - } - - const handleSelectAll = (checked: boolean) => { - if (checked) { - setSelectedTaskIds(new Set(filteredAndSortedTasks.map(task => task.id))) - } else { - setSelectedTaskIds(new Set()) - } - } - - const handleSelectTask = (taskId: string, checked: boolean) => { - const newSelected = new Set(selectedTaskIds) - if (checked) { - newSelected.add(taskId) - } else { - newSelected.delete(taskId) - } - setSelectedTaskIds(newSelected) - } - - const handleBulkDelete = async () => { - try { - const results = await Promise.all( - Array.from(selectedTaskIds).map(taskId => deleteTask(taskId)) - ) - - // Check if all deletions were successful - const allSuccessful = results.every(result => result === true) - - if (allSuccessful) { - setSelectedTaskIds(new Set()) - setDeleteDialogOpen(false) - } else { - console.error('Some tasks failed to delete') - // Clear selection of successfully deleted tasks - const failedTaskIds = Array.from(selectedTaskIds).filter( - (taskId, index) => !results[index] - ) - setSelectedTaskIds(new Set(failedTaskIds)) - } - } catch (error) { - console.error('Error deleting tasks:', error) - } - } - - const isAllSelected = - filteredAndSortedTasks.length > 0 && - filteredAndSortedTasks.every(task => selectedTaskIds.has(task.id)) - const isIndeterminate = selectedTaskIds.size > 0 && !isAllSelected - - 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' : ''} - - - - - {/* Bulk Actions Toolbar */} - {selectedTaskIds.size > 0 && ( - - - - {selectedTaskIds.size} task{selectedTaskIds.size > 1 ? 's' : ''}{' '} - selected - - - - - )} - - {/* Task Table */} - {filteredAndSortedTasks.length === 0 ? ( - - - No tasks found - - - {statusFilter === 'all' - ? 'Create your first task to get started!' - : `No ${statusFilter} tasks found.`} - - - ) : ( - - - - - - handleSelectAll(e.target.checked)} - inputProps={{ - 'aria-label': 'select all tasks', - }} - /> - - Title - {!isMobile && ( - Description - )} - - Status - - - Created - - {!isMobile && ( - - Completed - - )} - - - - {filteredAndSortedTasks.map(task => ( - - - - handleSelectTask(task.id, e.target.checked) - } - inputProps={{ - 'aria-labelledby': `task-${task.id}`, - }} - /> - - - - - {task.title} - - {isMobile && task.description && ( - - {task.description} - - )} - - - {!isMobile && ( - - - {task.description || '—'} - - - )} - - - - - - {formatCompactDate(task.created_at)} - - - {!isMobile && ( - - - {task.completed_at - ? formatCompactDate(task.completed_at) - : '—'} - - - )} - - ))} - -
-
- )} - - {/* Delete Confirmation Dialog */} - setDeleteDialogOpen(false)} - aria-labelledby="delete-dialog-title" - aria-describedby="delete-dialog-description" - > - - Delete {selectedTaskIds.size} task - {selectedTaskIds.size > 1 ? 's' : ''}? - - - - This action cannot be undone. Are you sure you want to delete the - selected task{selectedTaskIds.size > 1 ? 's' : ''}? - - - - - - - -
- ) -} diff --git a/frontend/app/hooks/useTasks.ts b/frontend/app/hooks/useTasks.ts index 3b91795..740d096 100644 --- a/frontend/app/hooks/useTasks.ts +++ b/frontend/app/hooks/useTasks.ts @@ -13,7 +13,6 @@ interface UseTasksState { interface UseTasksActions { fetchTasks: () => Promise createTask: (data: CreateTaskRequest) => Promise - deleteTask: (id: string) => Promise refreshTasks: () => Promise clearError: () => void getTaskById: (id: string) => Task | undefined @@ -23,19 +22,18 @@ interface UseTasksActions { interface UseTasksOptions { autoFetch?: boolean refreshInterval?: number - initialData?: Task[] } export function useTasks( options: UseTasksOptions = {} ): UseTasksState & UseTasksActions { - const { autoFetch = true, refreshInterval, initialData } = options + const { autoFetch = true, refreshInterval } = options const [state, setState] = useState({ - tasks: initialData || [], + tasks: [], loading: false, error: null, - lastFetch: initialData ? new Date() : null, + lastFetch: null, }) const clearError = useCallback(() => { @@ -91,27 +89,6 @@ export function useTasks( [] ) - const deleteTask = useCallback(async (id: string): Promise => { - try { - await apiClient.deleteTask(id) - - // Remove the task from the local state immediately - setState(prev => ({ - ...prev, - tasks: prev.tasks.filter(task => task.id !== id), - })) - - return true - } catch (error) { - const apiError = error as ApiError - setState(prev => ({ - ...prev, - error: apiError.message, - })) - return false - } - }, []) - const refreshTasks = useCallback(async () => { // Force refresh without showing loading state if tasks already exist const showLoading = state.tasks.length === 0 @@ -176,7 +153,6 @@ export function useTasks( ...state, fetchTasks, createTask, - deleteTask, refreshTasks, clearError, getTaskById, diff --git a/frontend/app/routes.ts b/frontend/app/routes.ts index c74f179..f8effb3 100644 --- a/frontend/app/routes.ts +++ b/frontend/app/routes.ts @@ -1,6 +1,3 @@ -import { type RouteConfig, index, route } from '@react-router/dev/routes' +import { type RouteConfig, index } from '@react-router/dev/routes' -export default [ - index('routes/home.tsx'), - route('*', 'routes/$.tsx'), -] satisfies RouteConfig +export default [index('routes/home.tsx')] satisfies RouteConfig diff --git a/frontend/app/routes/$.tsx b/frontend/app/routes/$.tsx deleted file mode 100644 index a567239..0000000 --- a/frontend/app/routes/$.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { redirect } from 'react-router' -import type { Route } from './+types/$' - -export async function loader({ request }: Route.LoaderArgs) { - const url = new URL(request.url) - - // Handle React DevTools and other development files - if ( - url.pathname.endsWith('.js.map') || - url.pathname.includes('installHook') || - url.pathname.startsWith('/__') || - url.pathname.startsWith('/node_modules/') - ) { - // Return a 404 response for these dev-only requests - throw new Response('Not Found', { status: 404 }) - } - - // For any other unmatched routes, redirect to home - return redirect('/') -} - -export default function CatchAll() { - // This component should never render since we always redirect or throw - return null -} diff --git a/frontend/app/routes/home.test.tsx b/frontend/app/routes/home.test.tsx index e15502f..80dd150 100644 --- a/frontend/app/routes/home.test.tsx +++ b/frontend/app/routes/home.test.tsx @@ -4,12 +4,13 @@ import Home from './home' describe('Home component', () => { it('should render task management interface', () => { - const mockLoaderData = { tasks: [] } - render() + render() + expect(screen.getByText(/Tasks/i)).toBeInTheDocument() expect( - screen.getByRole('heading', { level: 1, name: /Tasks/i }) + screen.getByText(/GTD-inspired task management system/i) + ).toBeInTheDocument() + expect( + screen.getByText(/Task Management Interface Coming Soon/i) ).toBeInTheDocument() - // TaskList component should be rendered with empty state - expect(screen.getByText(/No tasks found/i)).toBeInTheDocument() }) }) diff --git a/frontend/app/routes/home.tsx b/frontend/app/routes/home.tsx index bffbd4a..b818ac6 100644 --- a/frontend/app/routes/home.tsx +++ b/frontend/app/routes/home.tsx @@ -1,46 +1,43 @@ import type { Route } from './+types/home' import { Box, Typography, Container } from '@mui/material' -import { TaskList } from '~/components/TaskList' -import type { Task } from '~/types/task' export function meta(_: Route.MetaArgs) { return [ { title: "Captain's Log - Tasks" }, - { name: 'description', content: 'Task Dashboard' }, + { name: 'description', content: 'GTD-inspired task management system' }, ] } -export async function loader(): Promise<{ tasks: Task[] }> { - try { - // Fetch tasks from the backend API during SSR - const apiUrl = process.env.API_URL || 'http://localhost:3000' - const response = await fetch(`${apiUrl}/api/tasks`, { - headers: { - 'Content-Type': 'application/json', - }, - }) - - if (!response.ok) { - console.error('Failed to fetch tasks:', response.statusText) - return { tasks: [] } - } - - const tasks = await response.json() - return { tasks } - } catch (error) { - console.error('Error fetching tasks during SSR:', error) - return { tasks: [] } - } -} - -export default function Home({ loaderData }: Route.ComponentProps) { +export default function Home() { return ( Tasks - + + Your GTD-inspired task management system. Capture everything, see only + 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/frontend/vite.config.ts b/frontend/vite.config.ts index 5442d2b..d2e73cc 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -12,7 +12,4 @@ export default defineConfig({ }, }, }, - build: { - sourcemap: true, - }, }); diff --git a/plan/01_CORE_MVP/frontend.md b/plan/01_CORE_MVP/frontend.md index adbca39..325f4c7 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 -- [x] **Files**: `frontend/src/App.tsx`, `frontend/src/main.tsx` +- [ ] **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