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
76 changes: 75 additions & 1 deletion client/app/page.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useState, useEffect, useRef } from "react";
import { CheckCircle, Circle, CircleHalf, Microphone, Plus, WarningCircle, Fire, CaretDown } from "@phosphor-icons/react";
import { CheckCircle, Circle, CircleHalf, Microphone, Plus, WarningCircle, Fire, CaretDown, Trash } from "@phosphor-icons/react";
import { motion, AnimatePresence } from "motion/react";
import { fetchAPI } from "@/lib/api";
import { renderTaskItem } from "@/lib/taskRenderer";
Expand All @@ -14,6 +14,7 @@ export default function Dashboard() {
const [timeLeft, setTimeLeft] = useState("");
const [errorMsg, setErrorMsg] = useState("");
const [expandedFriends, setExpandedFriends] = useState(new Set());
const [deleteConfirm, setDeleteConfirm] = useState(null);

const recognitionRef = useRef(null);

Expand Down Expand Up @@ -139,6 +140,34 @@ export default function Dashboard() {
});
};

const showError = (message) => {
setErrorMsg(message);
setTimeout(() => setErrorMsg(""), 5000);
};

const performDeleteTask = async (taskId) => {
try {
const { status, data } = await fetchAPI(`/tasks/${taskId}`, {
method: "DELETE"
});

if (status === 200 && data.success) {
setTasks(prev => prev.filter(t => t._id !== taskId));
} else {
showError("Failed to delete task. Please try again.");
}
} catch (error) {
console.error("Error deleting task:", error);
showError("Failed to delete task. Please try again.");
}
setDeleteConfirm(null);
};

const deleteTask = (taskId, e) => {
e.stopPropagation();
setDeleteConfirm(taskId);
};

const stopListening = () => {
if (recognitionRef.current) {
recognitionRef.current.stop();
Expand Down Expand Up @@ -216,6 +245,44 @@ export default function Dashboard() {
)}
</AnimatePresence>

{/* Delete Confirmation Modal */}
<AnimatePresence>
{deleteConfirm && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center"
onClick={() => setDeleteConfirm(null)}
>
<motion.div
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
onClick={(e) => e.stopPropagation()}
className="glass-panel border border-border rounded-2xl p-6 max-w-sm mx-4"
>
<h3 className="text-lg font-medium text-white mb-2">Delete Task?</h3>
<p className="text-neutral-400 text-sm mb-6">This action cannot be undone. Are you sure you want to delete this task?</p>
<div className="flex gap-3 justify-end">
<button
onClick={() => setDeleteConfirm(null)}
className="px-4 py-2 rounded-lg text-neutral-300 hover:bg-surface transition-colors"
>
Cancel
</button>
<button
onClick={() => performDeleteTask(deleteConfirm)}
className="px-4 py-2 rounded-lg bg-red-500/20 text-red-400 hover:bg-red-500/30 transition-colors font-medium"
>
Delete
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>

{/* Main Tasks Area */}
<div className="lg:col-span-2">
<header className="mb-10">
Expand Down Expand Up @@ -303,6 +370,13 @@ export default function Dashboard() {
<span className={`flex-1 text-lg font-light transition-all duration-300 ${task.status === "completed" ? "text-neutral-500 line-through decoration-neutral-600" : "text-white"}`}>
{task.title}
</span>
<button
onClick={(e) => deleteTask(task._id, e)}
className="ml-3 p-2 text-neutral-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-all hover:bg-red-500/10 rounded-lg"
title="Delete task"
>
<Trash size={20} weight="bold" />
</button>
</motion.div>
))
)}
Expand Down
21 changes: 21 additions & 0 deletions server/src/controllers/task.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,24 @@ export const getAllFriendsTasks = async (req, res) => {
});
}
};

export const deleteTask = async (req, res) => {
try {
const { taskId } = req.params;

const task = await taskDao.deleteTaskById(req.user.id, taskId);

if (!task) {
return res
.status(404)
.json({ success: false, message: "Task not found" });
}

res.status(200).json({ success: true, message: "Task deleted successfully" });
} catch (error) {
console.error("Error in deleteTask:", error);
res
.status(500)
.json({ success: false, message: "Server error deleting task" });
}
};
6 changes: 6 additions & 0 deletions server/src/dao/task.dao.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ export const getTasksForUsersByDateRange = async (userIds, start, end) => {
.populate("userId", "name profilePicture streak")
.sort({ createdAt: -1 });
};

export const deleteTaskById = async (userId, taskId) => {
return await Task.findOneAndDelete(
{ _id: taskId, userId },
);
};
2 changes: 2 additions & 0 deletions server/src/routes/task.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getTaskHistory,
getFriendTasks,
getAllFriendsTasks,
deleteTask,
} from "../controllers/task.controller.js";

const taskRouter = Router();
Expand All @@ -19,6 +20,7 @@ taskRouter.get("/today", getTodayTasks);
taskRouter.get("/friends/today", getAllFriendsTasks);
taskRouter.get("/history", getTaskHistory);
taskRouter.patch("/:taskId/status", updateTaskStatus);
taskRouter.delete("/:taskId", deleteTask);
taskRouter.get("/friend/:friendId/today", getFriendTasks);

export default taskRouter;
Loading