Add task creation form. (#9)

Reviewed-on: #9
Co-authored-by: Drew Galbraith <drew@tiramisu.one>
Co-committed-by: Drew Galbraith <drew@tiramisu.one>
This commit is contained in:
Drew 2025-09-23 05:00:48 +00:00 committed by Drew
parent a683a071d1
commit b0916990fb
6 changed files with 286 additions and 16 deletions

View file

@ -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
</Typography>
<Button
component={Link}
to="/task/new"
variant="contained"
startIcon={<AddIcon />}
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
</Button>
</Toolbar>
</AppBar>
@ -198,19 +221,6 @@ export default function Layout({ children, loading = false }: LayoutProps) {
{children}
</Box>
{/* Quick capture FAB */}
<Fab
color="primary"
aria-label="add task"
sx={{
position: 'fixed',
bottom: 16,
right: 16,
zIndex: theme.zIndex.fab,
}}
>
<AddIcon />
</Fab>
</Box>
)
}

View file

@ -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<FormData>({
title: task?.title || '',
description: task?.description || '',
status: task?.status || TaskStatus.Todo,
})
const [errors, setErrors] = useState<FormErrors>({})
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<HTMLInputElement | HTMLTextAreaElement>) => {
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 = (
<Box component="form" onSubmit={handleSubmit} sx={{ width: '100%' }}>
<Typography variant="h4" component="h1" gutterBottom>
{task ? 'Edit Task' : 'Create New Task'}
</Typography>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<TextField
fullWidth
label="Title"
value={formData.title}
onChange={handleInputChange('title')}
error={!!errors.title}
helperText={errors.title}
margin="normal"
required
autoFocus
disabled={isSubmitting || loading}
/>
<TextField
fullWidth
label="Description"
value={formData.description}
onChange={handleInputChange('description')}
error={!!errors.description}
helperText={errors.description}
margin="normal"
multiline
rows={4}
disabled={isSubmitting || loading}
/>
{task && (
<FormControl fullWidth margin="normal">
<InputLabel>Status</InputLabel>
<Select
value={formData.status}
onChange={handleStatusChange}
label="Status"
disabled={isSubmitting || loading}
>
<MenuItem value={TaskStatus.Todo}>Todo</MenuItem>
<MenuItem value={TaskStatus.Done}>Done</MenuItem>
<MenuItem value={TaskStatus.Backlog}>Backlog</MenuItem>
</Select>
</FormControl>
)}
<Box sx={{ display: 'flex', gap: 2, mt: 3 }}>
<Button
type="submit"
variant="contained"
disabled={isSubmitting || loading}
sx={{ minWidth: 120 }}
>
{isSubmitting ? 'Saving...' : task ? 'Update' : 'Create'}
</Button>
<Button
type="button"
variant="outlined"
onClick={handleCancel}
disabled={isSubmitting || loading}
>
Cancel
</Button>
</Box>
</Box>
)
if (inline) {
return content
}
return (
<Paper elevation={2} sx={{ p: 3, maxWidth: 600, mx: 'auto' }}>
{content}
</Paper>
)
}