feat: add task detail slide-over panel with inline editing, subtasks, and notes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
108
TaskTracker.Web/src/components/SubtaskList.tsx
Normal file
108
TaskTracker.Web/src/components/SubtaskList.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { Plus, Square, CheckSquare } from 'lucide-react'
|
||||
import { WorkTaskStatus } from '../types/index.ts'
|
||||
import type { WorkTask } from '../types/index.ts'
|
||||
import { useCreateTask, useCompleteTask } from '../api/tasks.ts'
|
||||
|
||||
interface SubtaskListProps {
|
||||
taskId: number
|
||||
subtasks: WorkTask[]
|
||||
}
|
||||
|
||||
export default function SubtaskList({ taskId, subtasks }: SubtaskListProps) {
|
||||
const [showInput, setShowInput] = useState(false)
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const createTask = useCreateTask()
|
||||
const completeTask = useCompleteTask()
|
||||
|
||||
useEffect(() => {
|
||||
if (showInput) inputRef.current?.focus()
|
||||
}, [showInput])
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (e.key === 'Enter' && inputValue.trim()) {
|
||||
createTask.mutate(
|
||||
{ title: inputValue.trim(), parentTaskId: taskId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setInputValue('')
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
setShowInput(false)
|
||||
setInputValue('')
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggle(subtask: WorkTask) {
|
||||
if (subtask.status !== WorkTaskStatus.Completed) {
|
||||
completeTask.mutate(subtask.id)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-[#64748b]">
|
||||
Subtasks
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowInput(true)}
|
||||
className="p-1 rounded hover:bg-white/5 text-[#64748b] hover:text-white transition-colors"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
{subtasks.map((subtask) => {
|
||||
const isCompleted = subtask.status === WorkTaskStatus.Completed
|
||||
return (
|
||||
<div
|
||||
key={subtask.id}
|
||||
className="flex items-center gap-2 py-1.5 px-1 rounded hover:bg-white/5 cursor-pointer group"
|
||||
onClick={() => handleToggle(subtask)}
|
||||
>
|
||||
{isCompleted ? (
|
||||
<CheckSquare size={16} className="text-emerald-400 flex-shrink-0" />
|
||||
) : (
|
||||
<Square size={16} className="text-[#64748b] group-hover:text-white flex-shrink-0" />
|
||||
)}
|
||||
<span
|
||||
className={`text-sm ${
|
||||
isCompleted ? 'line-through text-[#64748b]' : 'text-white'
|
||||
}`}
|
||||
>
|
||||
{subtask.title}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{showInput && (
|
||||
<div className="flex items-center gap-2 py-1.5 px-1">
|
||||
<Square size={16} className="text-[#64748b] flex-shrink-0" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={() => {
|
||||
if (!inputValue.trim()) {
|
||||
setShowInput(false)
|
||||
setInputValue('')
|
||||
}
|
||||
}}
|
||||
placeholder="New subtask..."
|
||||
className="flex-1 bg-[#0f1117] text-sm text-white px-2 py-1 rounded border border-transparent focus:border-indigo-500 outline-none placeholder-[#64748b]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user