Files
TaskTracker/TaskTracker.Web/src/components/NotesList.tsx
2026-02-27 00:12:23 -05:00

127 lines
4.3 KiB
TypeScript

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<string, { label: string; bg: string; text: string }> = {
[NoteType.PauseNote]: { label: 'Pause', bg: 'bg-amber-500/10', text: 'text-amber-400' },
[NoteType.ResumeNote]: { label: 'Resume', bg: 'bg-blue-500/10', text: 'text-blue-400' },
[NoteType.General]: { label: 'General', bg: 'bg-white/5', text: 'text-[var(--color-text-secondary)]' },
}
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-[11px] font-medium text-[var(--color-text-secondary)] uppercase tracking-wider">
Notes
</h3>
<button
onClick={() => setShowInput(true)}
className="p-1 rounded hover:bg-white/5 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] 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-medium px-1.5 py-0.5 rounded ${typeConfig.bg} ${typeConfig.text}`}
>
{typeConfig.label}
</span>
<span className="text-[11px] text-[var(--color-text-tertiary)]">
{formatRelativeTime(note.createdAt)}
</span>
</div>
<p className="text-[var(--color-text-primary)] leading-relaxed">{note.content}</p>
</div>
)
})}
{sortedNotes.length === 0 && !showInput && (
<p className="text-sm text-[var(--color-text-secondary)] 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-[var(--color-page)] text-sm text-[var(--color-text-primary)] px-3 py-2 rounded border border-transparent focus:border-[var(--color-accent)] outline-none placeholder-[var(--color-text-secondary)]"
/>
)}
</div>
</div>
)
}