diff --git a/frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx b/frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx index 80a0c500..00b1737f 100644 --- a/frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx +++ b/frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx @@ -28,6 +28,8 @@ import { CopyIcon, Folder, PencilIcon, + Pin, + PinOff, Tag, Trash2Icon, XIcon, @@ -59,6 +61,8 @@ export const TaskDialog = ({ onMarkComplete, onMarkDeleted, isOverdue, + isPinned, + onTogglePin, }: EditTaskDialogProps) => { const handleDialogOpenChange = (open: boolean) => { if (open) { @@ -129,28 +133,33 @@ export const TaskDialog = ({ )} - + + {task.status === 'pending' && isOverdue(task.due) + ? 'O' : task.status === 'completed' - ? 'default' - : 'secondary' - } - > - {task.status === 'pending' && isOverdue(task.due) - ? 'O' - : task.status === 'completed' - ? 'C' - : task.status === 'deleted' - ? 'D' - : 'P'} - + ? 'C' + : task.status === 'deleted' + ? 'D' + : 'P'} + + {isPinned && ( + + )} + @@ -1233,6 +1242,23 @@ export const TaskDialog = ({ {/* Non-scrollable footer */} + {task.status == 'pending' ? ( diff --git a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx index 5043fba0..a2b62234 100644 --- a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx +++ b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx @@ -27,6 +27,8 @@ import { sortTasksById, getTimeSinceLastSync, hashKey, + getPinnedTasks, + togglePinnedTask, } from './tasks-utils'; import Pagination from './Pagination'; import { url } from '@/components/utils/URLs'; @@ -88,6 +90,7 @@ export const Tasks = ( const [lastSyncTime, setLastSyncTime] = useState(null); const tableRef = useRef(null); const [hotkeysEnabled, setHotkeysEnabled] = useState(false); + const [pinnedTasks, setPinnedTasks] = useState>(new Set()); const [selectedIndex, setSelectedIndex] = useState(0); const { state: editState, @@ -200,6 +203,11 @@ export const Tasks = ( } }, [props.email]); + // Load pinned tasks from localStorage + useEffect(() => { + setPinnedTasks(getPinnedTasks(props.email)); + }, [props.email]); + // Update the displayed time every 10 seconds useEffect(() => { const interval = setInterval(() => { @@ -403,6 +411,13 @@ export const Tasks = ( ); }; + const handleTogglePin = (taskUuid: string) => { + const newPinnedState = togglePinnedTask(props.email, taskUuid); + // Update the local state to trigger re-render + setPinnedTasks(getPinnedTasks(props.email)); + return newPinnedState; + }; + const handleSelectTask = (task: Task, index: number) => { setSelectedTask(task); setSelectedIndex(index); @@ -616,12 +631,19 @@ export const Tasks = ( } }; - const sortWithOverdueOnTop = (tasks: Task[]) => { + const sortWithPinnedAndOverdueOnTop = (tasks: Task[]) => { return [...tasks].sort((a, b) => { + const aPinned = pinnedTasks.has(a.uuid); + const bPinned = pinnedTasks.has(b.uuid); + + // Pinned tasks always on top + if (aPinned && !bPinned) return -1; + if (!aPinned && bPinned) return 1; + const aOverdue = a.status === 'pending' && isOverdue(a.due); const bOverdue = b.status === 'pending' && isOverdue(b.due); - // Overdue always on top + // Overdue tasks next (after pinned) if (aOverdue && !bOverdue) return -1; if (!aOverdue && bOverdue) return 1; @@ -676,9 +698,16 @@ export const Tasks = ( filteredTasks = results.map((r) => r.item); } - filteredTasks = sortWithOverdueOnTop(filteredTasks); + filteredTasks = sortWithPinnedAndOverdueOnTop(filteredTasks); setTempTasks(filteredTasks); - }, [selectedProjects, selectedTags, selectedStatuses, tasks, debouncedTerm]); + }, [ + selectedProjects, + selectedTags, + selectedStatuses, + tasks, + debouncedTerm, + pinnedTasks, + ]); const handleSaveTags = (task: Task, tags: string[]) => { const currentTags = tags || []; @@ -1018,6 +1047,8 @@ export const Tasks = ( onMarkComplete={handleMarkComplete} onMarkDeleted={handleMarkDelete} isOverdue={isOverdue} + isPinned={pinnedTasks.has(task.uuid)} + onTogglePin={handleTogglePin} /> )) )} diff --git a/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts b/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts index dcfde5fe..bccb5d9d 100644 --- a/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts +++ b/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts @@ -183,3 +183,53 @@ export const hashKey = (key: string, email: string): string => { } return Math.abs(hash).toString(36); }; + +/** + * Get the set of pinned task UUIDs from localStorage + */ +export const getPinnedTasks = (email: string): Set => { + const hashedKey = hashKey('pinnedTasks', email); + const stored = localStorage.getItem(hashedKey); + if (!stored) return new Set(); + try { + return new Set(JSON.parse(stored)); + } catch { + return new Set(); + } +}; + +/** + * Save the set of pinned task UUIDs to localStorage + */ +export const savePinnedTasks = ( + email: string, + pinnedUuids: Set +): void => { + const hashedKey = hashKey('pinnedTasks', email); + localStorage.setItem(hashedKey, JSON.stringify([...pinnedUuids])); +}; + +/** + * Toggle the pinned status of a task + * Returns the new pinned state + */ +export const togglePinnedTask = (email: string, taskUuid: string): boolean => { + const pinnedTasks = getPinnedTasks(email); + const isPinned = pinnedTasks.has(taskUuid); + + if (isPinned) { + pinnedTasks.delete(taskUuid); + } else { + pinnedTasks.add(taskUuid); + } + + savePinnedTasks(email, pinnedTasks); + return !isPinned; +}; + +/** + * Check if a task is pinned + */ +export const isTaskPinned = (email: string, taskUuid: string): boolean => { + return getPinnedTasks(email).has(taskUuid); +}; diff --git a/frontend/src/components/utils/types.ts b/frontend/src/components/utils/types.ts index 7555700b..402206cb 100644 --- a/frontend/src/components/utils/types.ts +++ b/frontend/src/components/utils/types.ts @@ -133,4 +133,6 @@ export interface EditTaskDialogProps { onMarkComplete: (uuid: string) => void; onMarkDeleted: (uuid: string) => void; isOverdue: (due?: string) => boolean; + isPinned: boolean; + onTogglePin: (uuid: string) => void; }