summaryrefslogtreecommitdiff
path: root/src/components/TaskList.tsx
diff options
context:
space:
mode:
authorSolstice <solstice@local>2026-06-09 00:22:18 -0700
committerSolstice <solstice@local>2026-06-09 00:22:18 -0700
commit6cb006cc136f1fc5c83537cc30c64d223d1755e4 (patch)
treeb12dfb4a8345c980dc8747657ce381016cdf3b34 /src/components/TaskList.tsx
parent25e1dcf205cd14feafdd9b4cf6b7a66f253ba6d2 (diff)
feat: frontend view, state management, and user interface
Diffstat (limited to 'src/components/TaskList.tsx')
-rw-r--r--src/components/TaskList.tsx283
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>
+ );
+}