Skip to content
Open
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
290 changes: 158 additions & 132 deletions frontend/src/components/Dashboard/DashboardTasks.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,54 @@
import { useNavigate } from "react-router-dom";
import { Plus, ArrowRight, CheckCircle2 } from "lucide-react";
import { useState } from "react";
import { ArrowRight } from "lucide-react";
import { useState, useCallback, useRef, useEffect } from "react";

// Toast popup component - shows at bottom right
function Toast({ message, type }) {
if (!message) return null;
return (
<div
className={`fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg text-white font-medium transition-all transform z-50 ${
type === "success" ? "bg-green-600" : "bg-red-600"
}`}
>
{message}
</div>
);
}

export default function DashboardTasks({ tasks, updateTask }) {
const navigate = useNavigate();
const [durationModalTask, setDurationModalTask] = useState(null);
const [actualDuration, setActualDuration] = useState("");
const [toast, setToast] = useState({ message: "", type: "success" });
const toastTimer = useRef(null);

const showToast = useCallback((message, type = "success") => {
clearTimeout(toastTimer.current);
setToast({ message, type });
toastTimer.current = setTimeout(() => setToast({ message: "", type: "success" }), 3000);
}, []);

const handleActualDurationSubmit = async () => {
const durationValue = Number(actualDuration);

if (Number.isNaN(durationValue) || durationValue <= 0) {
showToast("Please enter a valid duration in minutes", "error");
return;
}

try {
await updateTask(durationModalTask._id, {
status: "Completed",
actualDuration: durationValue,
});

setDurationModalTask(null);
setActualDuration("");
} catch (error) {
console.error("Failed to update task:", error);
}
};

const priorityOrder = {
High: 3,
Expand Down Expand Up @@ -53,147 +95,131 @@ export default function DashboardTasks({ tasks, updateTask }) {
}
};

const handleActualDurationSubmit = async () => {
const durationValue = Number(actualDuration);

if (Number.isNaN(durationValue) || durationValue <= 0) {
alert("Please enter a valid duration in minutes");
return;
}

try {
await updateTask(durationModalTask._id, {
status: "Completed",
actualDuration: durationValue,
});

setDurationModalTask(null);
setActualDuration("");
} catch (error) {
console.error("Failed to update task:", error);
}
};
useEffect(() => () => clearTimeout(toastTimer.current), []);

return (
<div className="card w-full">
{/* Header */}
<div className="flex justify-between items-center mb-5">
<div>
<h2 className="text-lg font-semibold text-main">Today’s Focus</h2>
<p className="text-xs text-muted">Top priorities for today</p>
<>
<Toast message={toast.message} type={toast.type} />
<div className="card w-full">
{/* Header */}
<div className="flex justify-between items-center mb-5">
<div>
<h2 className="text-lg font-semibold text-main">Today&apos;s Focus</h2>
<p className="text-xs text-muted">Top priorities for today</p>
</div>

<button
className="mt-3 group flex gap-2 self-center px-4 py-2 rounded-lg bg-(--primary) text-white text-sm font-medium hover:opacity-90 active:scale-95 transition-all duration-150 cursor-pointer"
onClick={() => navigate("/tasks")}
>
Manage <ArrowRight className="transition-transform duration-150 group-hover:translate-x-1" />
</button>
</div>

<button
className="mt-3 group flex gap-2 self-center px-4 py-2 rounded-lg bg-(--primary) text-white text-sm font-medium hover:opacity-90 active:scale-95 transition-all duration-150 cursor-pointer"
onClick={() => navigate("/tasks")}
>
Manage <ArrowRight className="transition-transform duration-150 group-hover:translate-x-1" />
</button>
</div>
{todayTasks?.length ? (
<div className="space-y-3">
{todayTasks.map((task) => (
<div
key={task._id}
className={`group relative flex items-center gap-4 border-l-4 rounded-xl p-4 transition-all duration-200
${priorityBorder[task.priority]}
bg-white/80 hover:bg-white dark:bg-slate-800/80 dark:hover:bg-slate-800 shadow-sm hover:shadow-md`}
>
{/* Checkbox */}
<input
type="checkbox"
className="h-4 w-4 accent-(--primary) cursor-pointer"
checked={task.status === "Completed"}
onChange={() => handleCheckboxChange(task)}
/>

{/* Task content */}
<div className="flex-1 min-w-0">
<p
className={`text-sm font-medium transition-colors break-words ${
task.status === "Completed"
? "line-through text-muted dark:text-gray-300"
: "text-main dark:text-white"
}`}
>
{task.title}
</p>

<div className="flex items-center gap-2 mt-1">
<span
className={`text-[11px] px-2 py-0.5 rounded-full font-medium ${
priorityBadge[task.priority]
}`}
>
{task.priority}
</span>

{task.status === "Completed" && (
<span className="text-[11px] text-muted dark:text-gray-300">Completed</span>
)}
</div>
</div>

{todayTasks?.length ? (
<div className="space-y-3">
{todayTasks.map((task) => (
<div
key={task._id}
className={`group relative flex items-center gap-4 border-l-4 rounded-xl p-4 transition-all duration-200
${priorityBorder[task.priority]}
bg-white/80 hover:bg-white dark:bg-slate-800/80 dark:hover:bg-slate-800 shadow-sm hover:shadow-md`}
{/* Hover affordance */}
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-xs text-muted dark:text-gray-300 opacity-0 group-hover:opacity-100 transition">
&#10003;
</span>
</div>
))}
</div>
) : (
<div className="text-sm text-muted dark:text-gray-300 text-center py-6 flex flex-col">
No tasks for today.

<button
className="mt-3 self-center px-4 py-2 rounded-lg bg-(--primary) text-white text-sm font-medium hover:opacity-90 active:scale-95 transition-all duration-150 cursor-pointer"
onClick={() => navigate("/tasks")}
>
{/* Checkbox */}
+ Add your first task
</button>
</div>
)}

{durationModalTask && (
<div className="fixed inset-0 bg-black/10 flex items-center justify-center z-50 animate-in fade-in duration-200">
<div className="bg-white dark:bg-[#141f2b] rounded-2xl shadow-xl w-full max-w-sm p-6 border border-soft dark:border-[#1e3a4a]">
<h2 className="text-xl font-semibold mb-2 text-black dark:text-white">
Complete Task
</h2>

<p className="text-sm mb-4 text-black/70 dark:text-white/70">
How long did you actually take to complete &quot;
{durationModalTask.title}&quot;?
</p>
<input
type="checkbox"
className="h-4 w-4 accent-(--primary) cursor-pointer"
checked={task.status === "Completed"}
onChange={() => handleCheckboxChange(task)}
type="number"
min="1"
value={actualDuration}
onChange={(e) => setActualDuration(e.target.value)}
className="w-full p-2 border border-soft rounded-lg text-black dark:text-white dark:bg-slate-800 dark:placeholder-slate-500"
placeholder="Actual duration in minutes"
/>

{/* Task content */}
<div className="flex-1 min-w-0">
<p
className={`text-sm font-medium transition-colors break-words ${
task.status === "Completed"
? "line-through text-muted dark:text-gray-300"
: "text-main dark:text-white"
}`}
<div className="flex justify-end gap-3 mt-5">
<button
onClick={() => {
setDurationModalTask(null);
setActualDuration("");
}}
className="px-4 py-2 rounded-lg border border-soft text-black dark:text-white dark:hover:bg-slate-800 hover:bg-gray-100 transition"
>
{task.title}
</p>

<div className="flex items-center gap-2 mt-1">
<span
className={`text-[11px] px-2 py-0.5 rounded-full font-medium ${
priorityBadge[task.priority]
}`}
>
{task.priority}
</span>

{task.status === "Completed" && (
<span className="text-[11px] text-muted dark:text-gray-300">Completed</span>
)}
</div>
Cancel
</button>
<button
onClick={handleActualDurationSubmit}
className="btn btn-primary px-4 py-2"
>
Mark Completed
</button>
</div>

{/* Hover affordance */}
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-xs text-muted dark:text-gray-300 opacity-0 group-hover:opacity-100 transition">
</span>
</div>
))}
</div>
) : (
<div className="text-sm text-muted dark:text-gray-300 text-center py-6 flex flex-col">
No tasks for today.

<button
className="mt-3 self-center px-4 py-2 rounded-lg bg-(--primary) text-white text-sm font-medium hover:opacity-90 active:scale-95 transition-all duration-150 cursor-pointer"
onClick={() => navigate("/tasks")}
>
+ Add your first task
</button>
</div>
)}

{durationModalTask && (
<div className="fixed inset-0 bg-black/10 flex items-center justify-center z-50 animate-in fade-in duration-200">
<div className="bg-white rounded-2xl shadow-xl w-full max-w-sm p-6">
<h2 className="text-xl font-semibold mb-2 text-black/90">
Complete Task
</h2>

<p className="text-sm mb-4 text-black">
How long did you actually take to complete "
{durationModalTask.title}"?
</p>
<input
type="number"
min="1"
value={actualDuration}
onChange={(e) => setActualDuration(e.target.value)}
className="w-full p-2 border border-soft rounded-lg text-black dark:placeholder-slate-500"
placeholder="Actual duration in minutes"
/>
<div className="flex justify-end gap-3 mt-5">
<button
onClick={() => {
setDurationModalTask(null);
setActualDuration("");
}}
className="px-4 py-2 rounded-lg border border-soft text-black hover:bg-gray-100 transition"
>
Cancel
</button>
<button
onClick={handleActualDurationSubmit}
className="btn btn-primary px-4 py-2"
>
Mark Completed
</button>
</div>
</div>
</div>
)}
</div>
)}
</div>
</>
);
}