diff options
| author | Solstice <solstice@local> | 2026-06-09 00:22:18 -0700 |
|---|---|---|
| committer | Solstice <solstice@local> | 2026-06-09 00:22:18 -0700 |
| commit | 6cb006cc136f1fc5c83537cc30c64d223d1755e4 (patch) | |
| tree | b12dfb4a8345c980dc8747657ce381016cdf3b34 /src/components/TaskList.tsx | |
| parent | 25e1dcf205cd14feafdd9b4cf6b7a66f253ba6d2 (diff) | |
feat: frontend view, state management, and user interface
Diffstat (limited to 'src/components/TaskList.tsx')
| -rw-r--r-- | src/components/TaskList.tsx | 283 |
1 files changed, 283 insertions, 0 deletions
diff --git a/src/components/TaskList.tsx b/src/components/TaskList.tsx new file mode 100644 index 0000000..0f87e0d --- /dev/null +++ b/src/components/TaskList.tsx @@ -0,0 +1,283 @@ +import { useState } from 'react'; +import { useTaskStore, Task } from '../store/taskStore'; +import { useTimerStore } from '../store/timerStore'; + +export function TaskList() { + const { tasks, addTask, deleteTask, setCurrentTask } = useTaskStore(); + const currentTaskId = useTimerStore((s) => s.currentTaskId); + + const [showForm, setShowForm] = useState(false); + const [newName, setNewName] = useState(''); + const [newSessions, setNewSessions] = useState(4); + + const handleAdd = async () => { + const name = newName.trim(); + if (!name) return; + await addTask(name, newSessions); + setNewName(''); + setNewSessions(4); + setShowForm(false); + }; + + const handleSelect = async (task: Task) => { + if (task.id === currentTaskId) { + await setCurrentTask(null); + } else { + await setCurrentTask(task.id); + } + }; + + const handleDelete = async (e: React.MouseEvent, id: string) => { + e.stopPropagation(); + await deleteTask(id); + }; + + return ( + <aside + style={{ + width: '280px', + flexShrink: 0, + background: 'var(--ink-1)', + borderRight: '1px solid var(--line-1)', + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + }} + > + {/* Header */} + <div + style={{ + padding: '16px', + borderBottom: '1px solid var(--line-1)', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + }} + > + <span className="eyebrow">Tasks</span> + <button + onClick={() => setShowForm((v) => !v)} + style={{ + fontFamily: 'var(--font-sans)', + fontSize: '12px', + fontWeight: 600, + padding: '4px 10px', + borderRadius: 'var(--r-1)', + border: '1px solid var(--line-2)', + background: 'transparent', + color: 'var(--brass)', + cursor: 'pointer', + transition: 'border-color 0.15s ease', + }} + onMouseEnter={(e) => { + (e.currentTarget as HTMLButtonElement).style.borderColor = 'var(--brass)'; + }} + onMouseLeave={(e) => { + (e.currentTarget as HTMLButtonElement).style.borderColor = 'var(--line-2)'; + }} + > + + Add + </button> + </div> + + {/* Add task form */} + {showForm && ( + <div + style={{ + padding: '12px 16px', + borderBottom: '1px solid var(--line-1)', + display: 'flex', + flexDirection: 'column', + gap: '8px', + }} + > + <input + type="text" + placeholder="Task name" + value={newName} + onChange={(e) => setNewName(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleAdd()} + autoFocus + style={{ + fontFamily: 'var(--font-sans)', + fontSize: '14px', + padding: '6px 10px', + borderRadius: 'var(--r-2)', + border: '1px solid var(--line-2)', + background: 'var(--ink-3)', + color: 'var(--fg-1)', + outline: 'none', + width: '100%', + boxSizing: 'border-box', + }} + onFocus={(e) => { + (e.currentTarget as HTMLInputElement).style.boxShadow = 'var(--shadow-brass)'; + }} + onBlur={(e) => { + (e.currentTarget as HTMLInputElement).style.boxShadow = 'none'; + }} + /> + <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}> + <label + style={{ + fontFamily: 'var(--font-sans)', + fontSize: '12px', + color: 'var(--fg-3)', + whiteSpace: 'nowrap', + }} + > + Sessions + </label> + <input + type="number" + min={1} + max={20} + value={newSessions} + onChange={(e) => setNewSessions(Math.max(1, parseInt(e.target.value) || 1))} + style={{ + fontFamily: 'var(--font-mono)', + fontSize: '14px', + padding: '4px 8px', + borderRadius: 'var(--r-2)', + border: '1px solid var(--line-2)', + background: 'var(--ink-3)', + color: 'var(--fg-1)', + outline: 'none', + width: '56px', + textAlign: 'center', + }} + /> + <button + onClick={handleAdd} + style={{ + fontFamily: 'var(--font-sans)', + fontSize: '12px', + fontWeight: 600, + padding: '4px 12px', + borderRadius: 'var(--r-1)', + border: 'none', + background: 'var(--brass)', + color: 'var(--brass-ink)', + cursor: 'pointer', + marginLeft: 'auto', + transition: 'background 0.15s ease', + }} + onMouseEnter={(e) => { + (e.currentTarget as HTMLButtonElement).style.background = 'var(--brass-bright)'; + }} + onMouseLeave={(e) => { + (e.currentTarget as HTMLButtonElement).style.background = 'var(--brass)'; + }} + > + Save + </button> + </div> + </div> + )} + + {/* Task list */} + <div style={{ overflowY: 'auto', flex: 1 }}> + {tasks.length === 0 && ( + <div + style={{ + padding: '24px 16px', + textAlign: 'center', + fontFamily: 'var(--font-sans)', + fontSize: '13px', + color: 'var(--fg-4)', + }} + > + No tasks yet + </div> + )} + {tasks.map((task) => { + const isSelected = task.id === currentTaskId; + const isCompleted = task.completed; + + return ( + <div + key={task.id} + onClick={() => !isCompleted && handleSelect(task)} + style={{ + padding: '10px 16px', + borderBottom: '1px solid var(--line-1)', + borderLeft: isSelected ? '2px solid var(--brass)' : '2px solid transparent', + display: 'flex', + alignItems: 'center', + gap: '8px', + cursor: isCompleted ? 'default' : 'pointer', + background: isSelected ? 'var(--ink-2)' : 'transparent', + opacity: isCompleted ? 0.45 : 1, + transition: 'background 0.15s ease, border-color 0.15s ease', + }} + onMouseEnter={(e) => { + if (!isSelected && !isCompleted) { + (e.currentTarget as HTMLDivElement).style.background = 'var(--ink-2)'; + } + }} + onMouseLeave={(e) => { + if (!isSelected) { + (e.currentTarget as HTMLDivElement).style.background = 'transparent'; + } + }} + > + {/* Task name */} + <div style={{ flex: 1, minWidth: 0 }}> + <div + style={{ + fontFamily: 'var(--font-sans)', + fontSize: '14px', + color: isCompleted ? 'var(--fg-4)' : 'var(--fg-2)', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }} + > + {task.name} + </div> + {/* Session count */} + <div + style={{ + fontFamily: 'var(--font-mono)', + fontSize: '11px', + color: 'var(--fg-4)', + fontVariantNumeric: 'tabular-nums', + marginTop: '2px', + }} + > + {task.remaining_sessions} / {task.total_sessions} + </div> + </div> + + {/* Delete */} + <button + onClick={(e) => handleDelete(e, task.id)} + style={{ + fontFamily: 'var(--font-sans)', + fontSize: '16px', + lineHeight: 1, + padding: '2px 4px', + border: 'none', + background: 'transparent', + color: 'var(--fg-4)', + cursor: 'pointer', + flexShrink: 0, + transition: 'color 0.15s ease', + }} + onMouseEnter={(e) => { + (e.currentTarget as HTMLButtonElement).style.color = 'var(--negative)'; + }} + onMouseLeave={(e) => { + (e.currentTarget as HTMLButtonElement).style.color = 'var(--fg-4)'; + }} + title="Remove task" + > + x + </button> + </div> + ); + })} + </div> + </aside> + ); +} |
