From b87cb69ae02d931a894feaf4173d741426135dda Mon Sep 17 00:00:00 2001 From: Hammer Date: Wed, 28 Jan 2026 18:32:37 +0000 Subject: [PATCH] Fix project loading, task detail panel, collapsible sidebar, due dates --- dist/index.html | 4 +- src/components/AddTask.tsx | 25 ++- src/components/Layout.tsx | 9 +- src/components/Sidebar.tsx | 113 +++++++++- src/components/TaskDetail.tsx | 377 ++++++++++++++++++++++++++++++++++ src/index.css | 14 ++ 6 files changed, 527 insertions(+), 15 deletions(-) create mode 100644 src/components/TaskDetail.tsx diff --git a/dist/index.html b/dist/index.html index e6c0cae..9780a95 100644 --- a/dist/index.html +++ b/dist/index.html @@ -6,8 +6,8 @@ Todo App - - + +
diff --git a/src/components/AddTask.tsx b/src/components/AddTask.tsx index 485c428..13d52b5 100644 --- a/src/components/AddTask.tsx +++ b/src/components/AddTask.tsx @@ -1,5 +1,5 @@ import { useState, useRef, useEffect } from 'react'; -import { Plus, Calendar, Flag, Tag, X, UserCircle } from 'lucide-react'; +import { Plus, Calendar, Flag, Tag, X } from 'lucide-react'; import type { Priority } from '@/types'; import { cn, getPriorityColor } from '@/lib/utils'; import { useTasksStore } from '@/stores/tasks'; @@ -117,25 +117,28 @@ export function AddTask({ projectId, sectionId, parentId, onClose, autoFocus = f {/* Options row */}
{/* Due date */} -
+
+ setDueDate(e.target.value)} - className="absolute inset-0 opacity-0 cursor-pointer" - /> - + /> + {dueDate && ( + + )}
{/* Priority selector */} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 2bf87a4..a23fa95 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,12 +1,13 @@ import { useEffect } from 'react'; import { Outlet, Navigate } from 'react-router-dom'; import { Sidebar } from './Sidebar'; +import { TaskDetail } from './TaskDetail'; import { useAuthStore } from '@/stores/auth'; import { useTasksStore } from '@/stores/tasks'; export function Layout() { const { isAuthenticated, isLoading } = useAuthStore(); - const { fetchProjects, fetchLabels } = useTasksStore(); + const { fetchProjects, fetchLabels, selectedTask, setSelectedTask } = useTasksStore(); useEffect(() => { if (isAuthenticated) { @@ -36,6 +37,12 @@ export function Layout() {
+ {selectedTask && ( + setSelectedTask(null)} + /> + )}
); } diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 7629a53..b8a12a0 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -2,7 +2,8 @@ import { useState, useRef, useEffect } from 'react'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import { Inbox, Calendar, CalendarDays, Plus, ChevronDown, ChevronRight, - Hash, Settings, LogOut, User, FolderPlus, Tag, X, Check + Hash, Settings, LogOut, User, FolderPlus, Tag, X, Check, + PanelLeftClose, PanelLeftOpen } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useAuthStore } from '@/stores/auth'; @@ -13,6 +14,8 @@ const PROJECT_COLORS = [ '#ec4899', '#06b6d4', '#f97316', '#6366f1', '#14b8a6', ]; +const SIDEBAR_COLLAPSED_KEY = 'sidebar-collapsed'; + export function Sidebar() { const location = useLocation(); const navigate = useNavigate(); @@ -24,6 +27,13 @@ export function Sidebar() { const [newProjectName, setNewProjectName] = useState(''); const [newProjectColor, setNewProjectColor] = useState(PROJECT_COLORS[0]); const [isCreatingProject, setIsCreatingProject] = useState(false); + const [isCollapsed, setIsCollapsed] = useState(() => { + try { + return localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === 'true'; + } catch { + return false; + } + }); const newProjectInputRef = useRef(null); useEffect(() => { @@ -32,6 +42,14 @@ export function Sidebar() { } }, [showNewProject]); + const toggleCollapsed = () => { + const next = !isCollapsed; + setIsCollapsed(next); + try { + localStorage.setItem(SIDEBAR_COLLAPSED_KEY, String(next)); + } catch {} + }; + const handleCreateProject = async () => { if (!newProjectName.trim() || isCreatingProject) return; setIsCreatingProject(true); @@ -67,6 +85,92 @@ export function Sidebar() { { path: '/today', icon: Calendar, label: 'Today', color: '#22c55e' }, { path: '/upcoming', icon: CalendarDays, label: 'Upcoming', color: '#8b5cf6' }, ]; + + // Collapsed sidebar + if (isCollapsed) { + return ( + + ); + } return (
+ diff --git a/src/components/TaskDetail.tsx b/src/components/TaskDetail.tsx new file mode 100644 index 0000000..4513816 --- /dev/null +++ b/src/components/TaskDetail.tsx @@ -0,0 +1,377 @@ +import { useState, useEffect, useRef } from 'react'; +import { + X, Check, Trash2, Calendar, Flag, User, FolderOpen, + Tag, MessageSquare, ChevronRight, AlertCircle +} from 'lucide-react'; +import type { Task, Priority, Comment } from '@/types'; +import { cn, formatDate, getPriorityColor, getPriorityLabel } from '@/lib/utils'; +import { useTasksStore } from '@/stores/tasks'; +import { api } from '@/lib/api'; + +interface TaskDetailProps { + task: Task; + onClose: () => void; +} + +export function TaskDetail({ task, onClose }: TaskDetailProps) { + const { updateTask, deleteTask, toggleComplete, projects, users, fetchUsers } = useTasksStore(); + const [title, setTitle] = useState(task.title); + const [description, setDescription] = useState(task.description || ''); + const [dueDate, setDueDate] = useState(task.dueDate || ''); + const [priority, setPriority] = useState(task.priority); + const [assigneeId, setAssigneeId] = useState(task.assigneeId || ''); + const [comments, setComments] = useState([]); + const [isDeleting, setIsDeleting] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const titleRef = useRef(null); + const panelRef = useRef(null); + + useEffect(() => { + setTitle(task.title); + setDescription(task.description || ''); + setDueDate(task.dueDate || ''); + setPriority(task.priority); + setAssigneeId(task.assigneeId || ''); + }, [task]); + + useEffect(() => { + if (users.length === 0) { + fetchUsers(); + } + }, []); + + useEffect(() => { + api.getComments(task.id).then(setComments).catch(() => setComments([])); + }, [task.id]); + + // Save on blur for title/description + const handleTitleBlur = () => { + if (title.trim() && title !== task.title) { + updateTask(task.id, { title: title.trim() }); + } + }; + + const handleDescriptionBlur = () => { + if (description !== (task.description || '')) { + updateTask(task.id, { description: description.trim() || undefined }); + } + }; + + const handleDueDateChange = (value: string) => { + setDueDate(value); + updateTask(task.id, { dueDate: value || undefined }); + }; + + const handlePriorityChange = (value: Priority) => { + setPriority(value); + updateTask(task.id, { priority: value }); + }; + + const handleAssigneeChange = (value: string) => { + setAssigneeId(value); + updateTask(task.id, { assigneeId: value || undefined }); + }; + + const handleDelete = async () => { + setIsDeleting(true); + try { + await deleteTask(task.id); + onClose(); + } catch (error) { + console.error('Failed to delete task:', error); + setIsDeleting(false); + } + }; + + const handleComplete = async () => { + await toggleComplete(task.id); + }; + + // Close on Escape + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [onClose]); + + const taskProject = projects.find(p => p.id === task.projectId); + + return ( + <> + {/* Backdrop */} +
+ + {/* Panel */} +
+ {/* Header */} +
+
+ + + {task.isCompleted ? 'Completed' : 'Mark complete'} + +
+ +
+ + {/* Content */} +
+
+ {/* Title */} + setTitle(e.target.value)} + onBlur={handleTitleBlur} + onKeyDown={(e) => e.key === 'Enter' && titleRef.current?.blur()} + className="w-full text-lg font-semibold text-gray-900 border-none outline-none placeholder-gray-400 bg-transparent" + placeholder="Task name" + /> + + {/* Description */} +