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:
2026-02-26 22:22:57 -05:00
parent f0bcc01993
commit af205b367d
5 changed files with 702 additions and 4 deletions

View File

@@ -0,0 +1,130 @@
import { useState, useRef, useEffect } from 'react'
import { Plus } from 'lucide-react'
import { NoteType } from '../types/index.ts'
import type { TaskNote } from '../types/index.ts'
import { useCreateNote } from '../api/notes.ts'
interface NotesListProps {
taskId: number
notes: TaskNote[]
}
const NOTE_TYPE_CONFIG: Record<number, { label: string; color: string }> = {
[NoteType.PauseNote]: { label: 'Pause', color: '#f59e0b' },
[NoteType.ResumeNote]: { label: 'Resume', color: '#6366f1' },
[NoteType.General]: { label: 'General', color: '#64748b' },
}
function formatRelativeTime(dateStr: string): string {
const date = new Date(dateStr)
const now = Date.now()
const diffMs = now - date.getTime()
const diffMins = Math.floor(diffMs / 60_000)
if (diffMins < 1) return 'just now'
if (diffMins < 60) return `${diffMins}m ago`
const diffHours = Math.floor(diffMins / 60)
if (diffHours < 24) return `${diffHours}h ago`
const diffDays = Math.floor(diffHours / 24)
if (diffDays === 1) return 'yesterday'
if (diffDays < 7) return `${diffDays}d ago`
const diffWeeks = Math.floor(diffDays / 7)
if (diffWeeks < 5) return `${diffWeeks}w ago`
return date.toLocaleDateString()
}
export default function NotesList({ taskId, notes }: NotesListProps) {
const [showInput, setShowInput] = useState(false)
const [inputValue, setInputValue] = useState('')
const inputRef = useRef<HTMLInputElement>(null)
const createNote = useCreateNote()
useEffect(() => {
if (showInput) inputRef.current?.focus()
}, [showInput])
// Chronological order (oldest first)
const sortedNotes = [...notes].sort(
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
)
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter' && inputValue.trim()) {
createNote.mutate(
{ taskId, content: inputValue.trim(), type: NoteType.General },
{
onSuccess: () => {
setInputValue('')
},
}
)
}
if (e.key === 'Escape') {
setShowInput(false)
setInputValue('')
}
}
return (
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-semibold uppercase tracking-wider text-[#64748b]">
Notes
</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-3">
{sortedNotes.map((note) => {
const typeConfig = NOTE_TYPE_CONFIG[note.type] ?? NOTE_TYPE_CONFIG[NoteType.General]
return (
<div key={note.id} className="text-sm">
<div className="flex items-center gap-2 mb-1">
<span
className="text-[10px] font-semibold uppercase px-1.5 py-0.5 rounded-full"
style={{
backgroundColor: typeConfig.color + '20',
color: typeConfig.color,
}}
>
{typeConfig.label}
</span>
<span className="text-[11px] text-[#64748b]">
{formatRelativeTime(note.createdAt)}
</span>
</div>
<p className="text-[#c4c9d4] leading-relaxed">{note.content}</p>
</div>
)
})}
{sortedNotes.length === 0 && !showInput && (
<p className="text-sm text-[#64748b] italic">No notes yet</p>
)}
{showInput && (
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={() => {
if (!inputValue.trim()) {
setShowInput(false)
setInputValue('')
}
}}
placeholder="Add a note..."
className="w-full bg-[#0f1117] text-sm text-white px-3 py-2 rounded border border-transparent focus:border-indigo-500 outline-none placeholder-[#64748b]"
/>
)}
</div>
</div>
)
}