Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 47 additions & 21 deletions frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
CopyIcon,
Folder,
PencilIcon,
Pin,
PinOff,
Tag,
Trash2Icon,
XIcon,
Expand Down Expand Up @@ -59,6 +61,8 @@ export const TaskDialog = ({
onMarkComplete,
onMarkDeleted,
isOverdue,
isPinned,
onTogglePin,
}: EditTaskDialogProps) => {
const handleDialogOpenChange = (open: boolean) => {
if (open) {
Expand Down Expand Up @@ -129,28 +133,33 @@ export const TaskDialog = ({
)}
</TableCell>
<TableCell className="py-2">
<Badge
className={
task.status === 'pending' && isOverdue(task.due)
? 'bg-orange-500 text-white'
: ''
}
variant={
task.status === 'deleted'
? 'destructive'
<div className="flex items-center gap-1">
<Badge
className={
task.status === 'pending' && isOverdue(task.due)
? 'bg-orange-500 text-white'
: ''
}
variant={
task.status === 'deleted'
? 'destructive'
: task.status === 'completed'
? 'default'
: 'secondary'
}
>
{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'}
</Badge>
? 'C'
: task.status === 'deleted'
? 'D'
: 'P'}
</Badge>
{isPinned && (
<Pin className="h-4 w-4 text-amber-500 fill-amber-500" />
)}
</div>
</TableCell>
</TableRow>
</DialogTrigger>
Expand Down Expand Up @@ -1233,6 +1242,23 @@ export const TaskDialog = ({

{/* Non-scrollable footer */}
<DialogFooter className="flex flex-row justify-end pt-4">
<Button
variant="outline"
onClick={() => onTogglePin(task.uuid)}
className="mr-auto"
>
{isPinned ? (
<>
<PinOff className="h-4 w-4 mr-1" />
Unpin
</>
) : (
<>
<Pin className="h-4 w-4 mr-1" />
Pin
</>
)}
</Button>
{task.status == 'pending' ? (
<Dialog>
<DialogTrigger asChild className="mr-5">
Expand Down
39 changes: 35 additions & 4 deletions frontend/src/components/HomeComponents/Tasks/Tasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
sortTasksById,
getTimeSinceLastSync,
hashKey,
getPinnedTasks,
togglePinnedTask,
} from './tasks-utils';
import Pagination from './Pagination';
import { url } from '@/components/utils/URLs';
Expand Down Expand Up @@ -88,6 +90,7 @@ export const Tasks = (
const [lastSyncTime, setLastSyncTime] = useState<number | null>(null);
const tableRef = useRef<HTMLDivElement>(null);
const [hotkeysEnabled, setHotkeysEnabled] = useState(false);
const [pinnedTasks, setPinnedTasks] = useState<Set<string>>(new Set());
const [selectedIndex, setSelectedIndex] = useState(0);
const {
state: editState,
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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 || [];
Expand Down Expand Up @@ -1018,6 +1047,8 @@ export const Tasks = (
onMarkComplete={handleMarkComplete}
onMarkDeleted={handleMarkDelete}
isOverdue={isOverdue}
isPinned={pinnedTasks.has(task.uuid)}
onTogglePin={handleTogglePin}
/>
))
)}
Expand Down
50 changes: 50 additions & 0 deletions frontend/src/components/HomeComponents/Tasks/tasks-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> => {
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<string>
): 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);
};
2 changes: 2 additions & 0 deletions frontend/src/components/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading