diff --git a/src/components/DesktopTaskItem.jsx b/src/components/DesktopTaskItem.jsx index e0a0d63..31969c6 100644 --- a/src/components/DesktopTaskItem.jsx +++ b/src/components/DesktopTaskItem.jsx @@ -124,4 +124,4 @@ const DesktopTaskItem = ({ ); }; -export default DesktopTaskItem; +export default React.memo(DesktopTaskItem); diff --git a/src/components/TaskBoard.jsx b/src/components/TaskBoard.jsx index 8c18e2f..f205e3c 100644 --- a/src/components/TaskBoard.jsx +++ b/src/components/TaskBoard.jsx @@ -1,5 +1,5 @@ import { ChevronDown, ChevronRight, MoreVertical, Plus } from "lucide-react"; -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import DesktopTaskItem from "./DesktopTaskItem"; const TaskBoard = ({ @@ -59,6 +59,25 @@ const TaskBoard = ({ return roots; }; + const boardData = useMemo( + () => + lists.map((list) => { + const listTasks = tasksByList[list.id] || []; + const activeTasks = listTasks.filter((t) => t.status !== "completed"); + const completedTasks = listTasks.filter( + (t) => t.status === "completed" + ); + + return { + list, + activeTree: buildTaskTree(activeTasks), + completedTasks, + completedTree: buildTaskTree(completedTasks), + }; + }), + [lists, tasksByList] + ); + // Recursive task renderer const renderTaskTree = (taskNode, level = 0) => { const childElements = taskNode.children?.map((child) => @@ -88,18 +107,9 @@ const TaskBoard = ({ return (
- {lists.map((list) => { - const listTasks = tasksByList[list.id] || []; - const activeTasks = listTasks.filter((t) => t.status !== "completed"); - const completedTasks = listTasks.filter( - (t) => t.status === "completed" - ); + {boardData.map(({ list, activeTree, completedTasks, completedTree }) => { const isExpanded = expandedLists[list.id]; - // Build trees - const activeTree = buildTaskTree(activeTasks); - const completedTree = buildTaskTree(completedTasks); - return (
{ @@ -125,6 +132,51 @@ const AppPage = ({ return roots; }; + const visibleTasks = useMemo(() => { + const filtered = showStarred + ? tasks.filter((t) => t.starred && t.status !== "completed") + : tasks.filter((t) => t.status !== "completed"); + return buildTaskTree(filtered); + }, [showStarred, tasks]); + + const completedTasks = useMemo( + () => + showStarred + ? tasks.filter((t) => t.starred && t.status === "completed") + : tasks.filter((t) => t.status === "completed"), + [showStarred, tasks] + ); + + const listTaskCounts = useMemo(() => { + const counts = {}; + taskLists.forEach((list) => { + counts[list.id] = (allTasksByList[list.id] || []).filter( + (task) => task.status !== "completed" + ).length; + }); + return counts; + }, [allTasksByList, taskLists]); + + const currentTaskList = useMemo( + () => taskLists.find((list) => list.id === currentListId), + [currentListId, taskLists] + ); + + const focusAddTaskInput = useCallback(() => { + requestAnimationFrame(() => { + addTaskInputRef.current?.focus(); + }); + }, []); + + const saveTasksCache = useCallback( + (listId, nextTasks) => { + if (!isDemoMode && api?.saveTasksToCache) { + api.saveTasksToCache(listId, nextTasks); + } + }, + [api, isDemoMode] + ); + // Toggle shortcuts const toggleShortcuts = () => { setShortcutsEnabled((prev) => { @@ -184,10 +236,7 @@ const AppPage = ({ ctrl: true, handler: () => { if (currentListId || viewMode === "list") { - const input = document.querySelector( - 'input[placeholder="Add a task"]' - ); - if (input) input.focus(); + focusAddTaskInput(); } }, }, @@ -551,19 +600,35 @@ const AppPage = ({ if (!inputValue.trim() || !currentListId) return; const tempId = `temp_${Date.now()}`; - const newTask = { id: tempId, title: inputValue, status: "needsAction" }; - setTasks([newTask, ...tasks]); + const newTask = { + id: tempId, + title: inputValue.trim(), + status: "needsAction", + }; + setTasks((prev) => { + const next = [newTask, ...prev]; + saveTasksCache(currentListId, next); + return next; + }); setInputValue(""); setIsSyncing(true); try { - await api.insertTask(currentListId, newTask.title); - // Reload tasks to get the real ID - const data = await api.getTasks(currentListId); - setTasks(data.items || []); + const createdTask = await api.insertTask(currentListId, newTask.title); + setTasks((prev) => { + const next = prev.map((task) => + task.id === tempId ? { ...createdTask, starred: false } : task + ); + saveTasksCache(currentListId, next); + return next; + }); } catch (e) { console.error("Failed to add task", e); - setTasks((prev) => prev.filter((t) => t.id !== tempId)); + setTasks((prev) => { + const next = prev.filter((t) => t.id !== tempId); + saveTasksCache(currentListId, next); + return next; + }); } finally { setIsSyncing(false); } @@ -716,10 +781,18 @@ const AppPage = ({ setIsSyncing(true); try { - // Pass parentId as the 5th parameter to insertTask - await api.insertTask(currentListId, subtaskInput, "", null, parentId); - const data = await api.getTasks(currentListId); - setTasks(data.items || []); + const createdSubtask = await api.insertTask( + currentListId, + subtaskInput.trim(), + "", + null, + parentId + ); + setTasks((prev) => { + const next = [createdSubtask, ...prev]; + saveTasksCache(currentListId, next); + return next; + }); setSubtaskInput(""); setAddingSubtaskToId(null); } catch (e) { @@ -997,13 +1070,7 @@ const AppPage = ({ setShowStarred(false); setCurrentListId(taskLists[0]?.id || null); setViewMode("list"); - // Focus on input after a short delay - setTimeout(() => { - const input = document.querySelector( - 'input[placeholder="Add a task"]' - ); - if (input) input.focus(); - }, 100); + focusAddTaskInput(); }} className="w-full flex items-center gap-3 px-4 py-3 bg-slate-200 dark:bg-slate-800 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-full text-slate-700 dark:text-slate-200 font-medium transition-colors shadow-sm" > @@ -1066,9 +1133,7 @@ const AppPage = ({ {isListsExpanded && (
{taskLists.map((list) => { - const listTaskCount = tasks.filter( - (t) => !showStarred - ).length; + const listTaskCount = listTaskCounts[list.id] || 0; // Check if this list is being edited if (editingListId === list.id) { @@ -1219,8 +1284,7 @@ const AppPage = ({ ? "All Tasks" : showStarred ? "Starred" - : taskLists.find((l) => l.id === currentListId)?.title || - "Select a list"} + : currentTaskList?.title || "Select a list"} {isSyncing && ( )} @@ -1292,7 +1356,7 @@ const AppPage = ({ onAddTask={(listId) => { setCurrentListId(listId); setViewMode("list"); - // Focus logic would go here + focusAddTaskInput(); }} onToggleTask={async (taskId, currentStatus) => { // Find which list this task belongs to @@ -1437,26 +1501,9 @@ const AppPage = ({
{/* Build task tree and render hierarchically */} {(() => { - const filteredTasks = showStarred - ? tasks.filter( - (t) => t.starred && t.status !== "completed" - ) - : tasks.filter((t) => t.status !== "completed"); - - const taskTree = buildTaskTree(filteredTasks); - - const renderTaskTree = ( - taskNode, - level = 0, - index = 0 - ) => { - const childElements = taskNode.children?.map( - (child, childIdx) => - renderTaskTree( - child, - level + 1, - index + childIdx + 1 - ) + const renderTaskTree = (taskNode, level = 0) => { + const childElements = taskNode.children?.map((child) => + renderTaskTree(child, level + 1) ); return ( @@ -1509,15 +1556,12 @@ const AppPage = ({ ); }; - return taskTree.map((taskNode) => + return visibleTasks.map((taskNode) => renderTaskTree(taskNode) ); })()} - {tasks.some( - (t) => - t.status === "completed" && (!showStarred || t.starred) - ) && ( + {completedTasks.length > 0 && ( <>
@@ -1536,25 +1580,14 @@ const AppPage = ({ )} Completed ( - { - tasks.filter( - (t) => - t.status === "completed" && - (!showStarred || t.starred) - ).length - } + {completedTasks.length} ) {isCompletedExpanded && (
- {(showStarred - ? tasks.filter( - (t) => t.starred && t.status === "completed" - ) - : tasks.filter((t) => t.status === "completed") - ).map((task) => ( + {completedTasks.map((task) => ( setInputValue(e.target.value)} />