feat: add inline create task form in Pending column

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 22:24:53 -05:00
parent af205b367d
commit b936d5ba7d
2 changed files with 145 additions and 12 deletions

View File

@@ -0,0 +1,125 @@
import { useRef, useState, useEffect } from 'react'
import { Loader2 } from 'lucide-react'
import { useCreateTask } from '../api/tasks.ts'
import { CATEGORY_COLORS } from '../lib/constants.ts'
interface CreateTaskFormProps {
onClose: () => void
}
const categories = Object.keys(CATEGORY_COLORS).filter((k) => k !== 'Unknown')
export default function CreateTaskForm({ onClose }: CreateTaskFormProps) {
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [category, setCategory] = useState('')
const [estimatedMinutes, setEstimatedMinutes] = useState('')
const titleRef = useRef<HTMLInputElement>(null)
const createTask = useCreateTask()
useEffect(() => {
titleRef.current?.focus()
}, [])
function handleSubmit() {
const trimmed = title.trim()
if (!trimmed) return
createTask.mutate(
{
title: trimmed,
description: description.trim() || undefined,
category: category || undefined,
estimatedMinutes: estimatedMinutes ? Number(estimatedMinutes) : undefined,
},
{ onSuccess: () => onClose() }
)
}
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === 'Escape') {
e.preventDefault()
onClose()
}
if (e.key === 'Enter' && (e.target as HTMLElement).tagName !== 'TEXTAREA') {
e.preventDefault()
handleSubmit()
}
}
const inputClass =
'w-full rounded-md bg-[#0f1117] text-white text-sm px-3 py-2 border border-white/10 placeholder-[#64748b] focus:outline-none focus:ring-2 focus:ring-indigo-500/60 focus:border-transparent transition-colors'
return (
<div
className="rounded-lg bg-[#1a1d27] p-3 space-y-3"
onKeyDown={handleKeyDown}
>
{/* Title */}
<input
ref={titleRef}
type="text"
placeholder="Task title"
value={title}
onChange={(e) => setTitle(e.target.value)}
className={inputClass}
/>
{/* Description */}
<textarea
placeholder="Description (optional)"
rows={2}
value={description}
onChange={(e) => setDescription(e.target.value)}
className={`${inputClass} resize-none`}
/>
{/* Category + Estimated Minutes row */}
<div className="flex gap-2">
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className={`${inputClass} flex-1 appearance-none cursor-pointer`}
>
<option value="">Category</option>
{categories.map((cat) => (
<option key={cat} value={cat}>
{cat}
</option>
))}
</select>
<input
type="number"
placeholder="Est. min"
min={1}
value={estimatedMinutes}
onChange={(e) => setEstimatedMinutes(e.target.value)}
className={`${inputClass} w-24`}
/>
</div>
{/* Actions */}
<div className="flex items-center justify-end gap-2 pt-1">
<button
type="button"
onClick={onClose}
className="text-xs text-[#64748b] hover:text-white transition-colors px-2 py-1"
>
Cancel
</button>
<button
type="button"
onClick={handleSubmit}
disabled={!title.trim() || createTask.isPending}
className="flex items-center gap-1.5 text-xs font-medium text-white bg-indigo-600 hover:bg-indigo-500
disabled:opacity-40 disabled:cursor-not-allowed
px-3 py-1.5 rounded-md transition-colors"
>
{createTask.isPending && <Loader2 size={12} className="animate-spin" />}
Create
</button>
</div>
</div>
)
}

View File

@@ -1,8 +1,10 @@
import { useState } from 'react'
import { useDroppable } from '@dnd-kit/core'
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
import { Plus } from 'lucide-react'
import type { WorkTask } from '../types/index.ts'
import TaskCard from './TaskCard.tsx'
import CreateTaskForm from './CreateTaskForm.tsx'
interface KanbanColumnProps {
status: number
@@ -19,9 +21,9 @@ export default function KanbanColumn({
color,
tasks,
onTaskClick,
onAddTask,
}: KanbanColumnProps) {
const { setNodeRef, isOver } = useDroppable({ id: `column-${status}` })
const [showForm, setShowForm] = useState(false)
const taskIds = tasks.map((t) => t.id)
@@ -62,17 +64,23 @@ export default function KanbanColumn({
</SortableContext>
</div>
{/* Add task button (Pending column only) */}
{status === 0 && onAddTask && (
<button
onClick={onAddTask}
className="flex items-center justify-center gap-1.5 mx-3 mb-3 py-2 rounded-lg
text-xs text-[#64748b] border border-dashed border-white/10
hover:text-white hover:border-white/20 transition-colors duration-150"
>
<Plus size={14} />
Add Task
</button>
{/* Add task form / button (Pending column only) */}
{status === 0 && (
<div className="px-3 pb-3">
{showForm ? (
<CreateTaskForm onClose={() => setShowForm(false)} />
) : (
<button
onClick={() => setShowForm(true)}
className="flex items-center justify-center gap-1.5 w-full py-2 rounded-lg
text-xs text-[#64748b] border border-dashed border-white/10
hover:text-white hover:border-white/20 transition-colors duration-150"
>
<Plus size={14} />
Add Task
</button>
)}
</div>
)}
</div>
)