diff --git a/CLAUDE.md b/CLAUDE.md
index a84b622..7f3a193 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -80,6 +80,8 @@ tasks (id, title, description, status, created_at, updated_at, completed_at)
- `just check` - Validate all changes (primary validation command)
- `just fmt` - Format code (resolve formatting errors)
+ALWAYS USE `just check` TO VALIDATE YOUR CHANGES INSTEAD OF LANGUAGE SPECIFIC COMMANDS.
+
## Current Phase: Core MVP Backend ✅
**Status**: Backend implementation completed
@@ -98,4 +100,4 @@ See `/plan/future_improvements.md` for architectural enhancement opportunities.
2. **Phase 3**: Project organization and hierarchical structure
3. **Phase 4**: Smart views and anti-overwhelm features
4. **Phase 5**: Recurring tasks and advanced scheduling
-5. **Phase 6**: PWA features and offline capability
\ No newline at end of file
+5. **Phase 6**: PWA features and offline capability
diff --git a/frontend/app/components/Layout.tsx b/frontend/app/components/Layout.tsx
index 169e9f5..9117881 100644
--- a/frontend/app/components/Layout.tsx
+++ b/frontend/app/components/Layout.tsx
@@ -13,8 +13,8 @@ import {
Toolbar,
Typography,
useTheme,
- Fab,
LinearProgress,
+ Button,
} from '@mui/material'
import {
Menu as MenuIcon,
@@ -22,6 +22,7 @@ import {
Add as AddIcon,
Settings as SettingsIcon,
} from '@mui/icons-material'
+import { Link } from 'react-router'
const drawerWidth = 240
@@ -137,6 +138,28 @@ export default function Layout({ children, loading = false }: LayoutProps) {
>
⚓ Captain's Log
+
+ }
+ sx={{
+ backgroundColor: theme.palette.secondary.main,
+ color: theme.palette.secondary.contrastText,
+ '&:hover': {
+ backgroundColor: theme.palette.secondary.dark || '#b7791f',
+ },
+ fontWeight: 600,
+ fontSize: '1rem',
+ px: 2,
+ py: 1.5,
+ minWidth: 'auto',
+ height: '44px',
+ }}
+ >
+ New Task
+
@@ -198,19 +221,6 @@ export default function Layout({ children, loading = false }: LayoutProps) {
{children}
- {/* Quick capture FAB */}
-
-
-
)
}
diff --git a/frontend/app/components/TaskForm.tsx b/frontend/app/components/TaskForm.tsx
new file mode 100644
index 0000000..8da2698
--- /dev/null
+++ b/frontend/app/components/TaskForm.tsx
@@ -0,0 +1,231 @@
+import { useState } from 'react'
+import {
+ Box,
+ TextField,
+ Button,
+ FormControl,
+ InputLabel,
+ Select,
+ MenuItem,
+ Paper,
+ Typography,
+ Alert,
+} from '@mui/material'
+import { useNavigate } from 'react-router'
+import type { CreateTaskRequest, Task } from '~/types/task'
+import { TaskStatus } from '~/types/task'
+import { useTasks } from '~/hooks/useTasks'
+
+interface TaskFormProps {
+ task?: Task
+ onSuccess?: (task: Task) => void
+ onCancel?: () => void
+ inline?: boolean
+}
+
+interface FormData {
+ title: string
+ description: string
+ status: TaskStatus
+}
+
+interface FormErrors {
+ title?: string
+ description?: string
+}
+
+export function TaskForm({
+ task,
+ onSuccess,
+ onCancel,
+ inline = false,
+}: TaskFormProps) {
+ const navigate = useNavigate()
+ const { createTask, loading, error } = useTasks({ autoFetch: false })
+
+ const [formData, setFormData] = useState({
+ title: task?.title || '',
+ description: task?.description || '',
+ status: task?.status || TaskStatus.Todo,
+ })
+
+ const [errors, setErrors] = useState({})
+ const [isSubmitting, setIsSubmitting] = useState(false)
+
+ const validateForm = (): boolean => {
+ const newErrors: FormErrors = {}
+
+ if (!formData.title.trim()) {
+ newErrors.title = 'Title is required'
+ } else if (formData.title.trim().length < 3) {
+ newErrors.title = 'Title must be at least 3 characters long'
+ } else if (formData.title.trim().length > 200) {
+ newErrors.title = 'Title must be less than 200 characters'
+ }
+
+ if (formData.description.length > 1000) {
+ newErrors.description = 'Description must be less than 1000 characters'
+ }
+
+ setErrors(newErrors)
+ return Object.keys(newErrors).length === 0
+ }
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+
+ if (!validateForm()) {
+ return
+ }
+
+ setIsSubmitting(true)
+
+ try {
+ if (task) {
+ // Update existing task - for now we only support create
+ // This would use updateTask hook when implemented
+ console.log('Update not implemented yet')
+ } else {
+ // Create new task
+ const createData: CreateTaskRequest = {
+ title: formData.title.trim(),
+ description: formData.description.trim() || undefined,
+ }
+
+ const newTask = await createTask(createData)
+
+ if (newTask) {
+ if (onSuccess) {
+ onSuccess(newTask)
+ } else {
+ navigate('/')
+ }
+ }
+ }
+ } catch (err) {
+ console.error('Form submission error:', err)
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ const handleCancel = () => {
+ if (onCancel) {
+ onCancel()
+ } else {
+ navigate('/')
+ }
+ }
+
+ const handleInputChange =
+ (field: keyof FormData) =>
+ (e: React.ChangeEvent) => {
+ setFormData(prev => ({
+ ...prev,
+ [field]: e.target.value,
+ }))
+
+ // Clear error for this field when user starts typing
+ if (errors[field as keyof FormErrors]) {
+ setErrors(prev => ({
+ ...prev,
+ [field]: undefined,
+ }))
+ }
+ }
+
+ const handleStatusChange = (
+ e: React.ChangeEvent<{ name?: string; value: unknown }>
+ ) => {
+ setFormData(prev => ({
+ ...prev,
+ status: e.target.value as TaskStatus,
+ }))
+ }
+
+ const content = (
+
+
+ {task ? 'Edit Task' : 'Create New Task'}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+ {task && (
+
+ Status
+
+
+ )}
+
+
+
+
+
+
+
+ )
+
+ if (inline) {
+ return content
+ }
+
+ return (
+
+ {content}
+
+ )
+}
diff --git a/frontend/app/routes.ts b/frontend/app/routes.ts
index c74f179..a60a27f 100644
--- a/frontend/app/routes.ts
+++ b/frontend/app/routes.ts
@@ -2,5 +2,6 @@ import { type RouteConfig, index, route } from '@react-router/dev/routes'
export default [
index('routes/home.tsx'),
+ route('task/new', 'routes/task.new.tsx'),
route('*', 'routes/$.tsx'),
] satisfies RouteConfig
diff --git a/frontend/app/routes/home.test.tsx b/frontend/app/routes/home.test.tsx
index e15502f..e5fd6ca 100644
--- a/frontend/app/routes/home.test.tsx
+++ b/frontend/app/routes/home.test.tsx
@@ -1,14 +1,20 @@
import { render, screen } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
+import { MemoryRouter } from 'react-router'
import Home from './home'
describe('Home component', () => {
it('should render task management interface', () => {
const mockLoaderData = { tasks: [] }
- render()
+ render(
+
+
+
+ )
expect(
screen.getByRole('heading', { level: 1, name: /Tasks/i })
).toBeInTheDocument()
+
// TaskList component should be rendered with empty state
expect(screen.getByText(/No tasks found/i)).toBeInTheDocument()
})
diff --git a/frontend/app/routes/task.new.tsx b/frontend/app/routes/task.new.tsx
new file mode 100644
index 0000000..2d15c4a
--- /dev/null
+++ b/frontend/app/routes/task.new.tsx
@@ -0,0 +1,20 @@
+import type { Route } from './+types/task.new'
+import { Container, Box } from '@mui/material'
+import { TaskForm } from '~/components/TaskForm'
+
+export function meta(_: Route.MetaArgs) {
+ return [
+ { title: "Captain's Log - New Task" },
+ { name: 'description', content: 'Create a new task' },
+ ]
+}
+
+export default function NewTask() {
+ return (
+
+
+
+
+
+ )
+}