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:
130
TaskTracker.Web/src/components/NotesList.tsx
Normal file
130
TaskTracker.Web/src/components/NotesList.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user