feat: add inline create task form in Pending column
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
125
TaskTracker.Web/src/components/CreateTaskForm.tsx
Normal file
125
TaskTracker.Web/src/components/CreateTaskForm.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user