127 lines
4.3 KiB
TypeScript
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>
|
|
)
|
|
}
|