Skip to content
Merged
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
151 changes: 97 additions & 54 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 @@ -70,6 +72,8 @@ export const TaskDialog = ({
onMarkDeleted,
isOverdue,
isUnsynced,
isPinned,
onTogglePin,
}: EditTaskDialogProps) => {
const editButtonRef = useRef<
Partial<Record<FieldKey, HTMLButtonElement | null>>
Expand Down Expand Up @@ -199,7 +203,10 @@ export const TaskDialog = ({
onSelectTask(task, index);
}}
>
<TableCell className="py-2" onClick={(e) => e.stopPropagation()}>
<TableCell
className="py-3 md:py-2 align-top"
onClick={(e) => e.stopPropagation()}
>
<input
type="checkbox"
checked={selectedTaskUUIDs.includes(task.uuid)}
Expand All @@ -211,59 +218,77 @@ export const TaskDialog = ({
/>
</TableCell>

{/* Display task details */}
<TableCell className="py-2">
<span
className={`px-3 py-1 rounded-md font-semibold ${
task.status === 'pending' && isOverdue(task.due)
? 'bg-red-600/80 text-white'
: 'dark:text-white text-black'
}`}
>
{task.id}
</span>
</TableCell>
<TableCell className="flex items-center space-x-2 py-2">
{task.priority === 'H' && (
<div className="flex items-center justify-center w-3 h-3 bg-red-500 rounded-full border-0 min-w-3"></div>
)}
{task.priority === 'M' && (
<div className="flex items-center justify-center w-3 h-3 bg-yellow-500 rounded-full border-0 min-w-3"></div>
)}
{task.priority != 'H' && task.priority != 'M' && (
<div className="flex items-center justify-center w-3 h-3 bg-green-500 rounded-full border-0 min-w-3"></div>
)}
<span className="text-s text-foreground">{task.description}</span>
{task.project != '' && (
<Badge variant={'secondary'}>
<Folder className="pr-2" />
{task.project === '' ? '' : task.project}
</Badge>
)}
</TableCell>
<TableCell className="py-2">
<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'
? 'C'
: task.status === 'deleted'
? 'D'
: 'P'}
</Badge>
{/* Desktop: single row layout, Mobile: 2-row layout */}
<TableCell className="py-2" colSpan={3}>
<div className="flex items-center gap-4">
<span
className={`px-3 py-1 rounded-md font-semibold ${
task.status === 'pending' && isOverdue(task.due)
? 'bg-red-600/80 text-white'
: 'dark:text-white text-black'
}`}
>
{task.id}
</span>
<div className="flex items-center space-x-2 flex-1 min-w-0">
{task.priority === 'H' && (
<div className="flex items-center justify-center w-3 h-3 bg-red-500 rounded-full border-0 min-w-3"></div>
)}
{task.priority === 'M' && (
<div className="flex items-center justify-center w-3 h-3 bg-yellow-500 rounded-full border-0 min-w-3"></div>
)}
{task.priority != 'H' && task.priority != 'M' && (
<div className="flex items-center justify-center w-3 h-3 bg-green-500 rounded-full border-0 min-w-3"></div>
)}
<span className="text-s text-foreground">
{task.description}
</span>
{task.project != '' && (
<Badge variant={'secondary'}>
<Folder className="pr-2" />
{task.project === '' ? '' : task.project}
</Badge>
)}
</div>
<div className="flex items-center gap-2">
<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'
? 'C'
: task.status === 'deleted'
? 'D'
: 'P'}
</Badge>
<button
onClick={(e) => {
e.stopPropagation();
onTogglePin(task.uuid);
}}
className="p-1 hover:bg-muted rounded transition-colors"
aria-label={isPinned ? 'Unpin task' : 'Pin task'}
>
{isPinned ? (
<Pin className="h-4 w-4 text-amber-500 fill-amber-500" />
) : (
<Pin className="h-4 w-4 text-muted-foreground" />
)}
</button>
</div>
</div>
</TableCell>
</TableRow>
</DialogTrigger>
Expand Down Expand Up @@ -1688,6 +1713,24 @@ export const TaskDialog = ({
</div>

<DialogFooter className="flex flex-row justify-end pt-4">
<Button
Comment thread
Rustix69 marked this conversation as resolved.
variant="outline"
onClick={() => onTogglePin(task.uuid)}
className="mr-auto"
aria-label={isPinned ? 'Unpin task' : 'Pin task'}
>
{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
37 changes: 34 additions & 3 deletions frontend/src/components/HomeComponents/Tasks/Tasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import {
getTimeSinceLastSync,
Comment thread
Rustix69 marked this conversation as resolved.
hashKey,
isOverdue,
getPinnedTasks,
togglePinnedTask,
} from './tasks-utils';
import Pagination from './Pagination';
import { url } from '@/components/utils/URLs';
Expand Down Expand Up @@ -103,6 +105,7 @@ export const Tasks = (
const [searchTerm, setSearchTerm] = useState('');
const [debouncedTerm, setDebouncedTerm] = useState('');
const [lastSyncTime, setLastSyncTime] = useState<number | null>(null);
const [pinnedTasks, setPinnedTasks] = useState<Set<string>>(new Set());
const [selectedTaskUUIDs, setSelectedTaskUUIDs] = useState<string[]>([]);
const [unsyncedTaskUuids, setUnsyncedTaskUuids] = useState<Set<string>>(
new Set()
Expand Down Expand Up @@ -201,6 +204,11 @@ export const Tasks = (
}
}, [props.email]);

// Load pinned tasks from localStorage
useEffect(() => {
setPinnedTasks(getPinnedTasks(props.email));
}, [props.email]);

useEffect(() => {
const interval = setInterval(() => {
setLastSyncTime((prevTime) => prevTime);
Expand Down Expand Up @@ -479,6 +487,12 @@ export const Tasks = (
);
};

const handleTogglePin = (taskUuid: string) => {
togglePinnedTask(props.email, taskUuid);
// Update the local state to trigger re-render
setPinnedTasks(getPinnedTasks(props.email));
};

const handleSelectTask = (task: Task, index: number) => {
setSelectedTask(task);
setSelectedIndex(index);
Expand Down Expand Up @@ -739,11 +753,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 tasks next (after pinned)
if (aOverdue && !bOverdue) return -1;
if (!aOverdue && bOverdue) return 1;

Expand Down Expand Up @@ -795,9 +817,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 @@ -1218,6 +1247,8 @@ export const Tasks = (
onMarkDeleted={handleMarkDelete}
isOverdue={isOverdue}
isUnsynced={unsyncedTaskUuids.has(task.uuid)}
isPinned={pinnedTasks.has(task.uuid)}
onTogglePin={handleTogglePin}
/>
))
)}
Expand Down
Loading
Loading