diff --git a/backend/models/Task.js b/backend/models/Task.js new file mode 100644 index 0000000..dfc56ba --- /dev/null +++ b/backend/models/Task.js @@ -0,0 +1,16 @@ +const mongoose = require('mongoose'); + +const TaskSchema = new mongoose.Schema( + { + title: { type: String, required: true, trim: true }, + description: { type: String, default: '' }, + status: { type: String, enum: ['pending', 'completed'], default: 'pending' }, + deadline: { type: Date }, + userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, + }, + { timestamps: true } +); + +module.exports = mongoose.model('Task', TaskSchema); + + diff --git a/backend/routes/tasks.route.js b/backend/routes/tasks.route.js new file mode 100644 index 0000000..9e49627 --- /dev/null +++ b/backend/routes/tasks.route.js @@ -0,0 +1,87 @@ +const express = require('express'); +const router = express.Router(); +const auth = require('../middleware/auth'); +const Task = require('../models/Task'); + +// POST /api/tasks - Create a new task +router.post('/', auth, async (req, res) => { + try { + const { title, description, status, deadline } = req.body; + if (!title || !title.trim()) { + return res.status(400).json({ errors: [{ msg: 'Title is required' }] }); + } + const task = new Task({ + title: title.trim(), + description: description || '', + status: status === 'completed' ? 'completed' : 'pending', + deadline: deadline ? new Date(deadline) : undefined, + userId: req.user.id, + }); + const saved = await task.save(); + return res.status(201).json(saved); + } catch (err) { + console.error('Create task error:', err); + return res.status(500).json({ errors: [{ msg: 'Server error' }] }); + } +}); + +// GET /api/tasks - Get tasks for the authenticated user +router.get('/', auth, async (req, res) => { + try { + const tasks = await Task.find({ userId: req.user.id }).sort({ createdAt: -1 }); + return res.json(tasks); + } catch (err) { + console.error('Fetch tasks error:', err); + return res.status(500).json({ errors: [{ msg: 'Server error' }] }); + } +}); + +// PUT /api/tasks/:id - Update a task (only owner) +router.put('/:id', auth, async (req, res) => { + try { + const { id } = req.params; + const updates = {}; + if (typeof req.body.title === 'string') updates.title = req.body.title.trim(); + if (typeof req.body.description === 'string') updates.description = req.body.description; + if (typeof req.body.status === 'string' && ['pending','completed'].includes(req.body.status)) updates.status = req.body.status; + if (typeof req.body.deadline !== 'undefined') updates.deadline = req.body.deadline ? new Date(req.body.deadline) : null; + // Never allow changing userId via API + + const task = await Task.findOne({ _id: id, userId: req.user.id }); + if (!task) { + return res.status(404).json({ errors: [{ msg: 'Task not found' }] }); + } + + Object.assign(task, updates); + const saved = await task.save(); + return res.json(saved); + } catch (err) { + console.error('Update task error:', err); + if (err.name === 'CastError') { + return res.status(400).json({ errors: [{ msg: 'Invalid task id' }] }); + } + return res.status(500).json({ errors: [{ msg: 'Server error' }] }); + } +}); + +// DELETE /api/tasks/:id - Delete a task (only owner) +router.delete('/:id', auth, async (req, res) => { + try { + const { id } = req.params; + const task = await Task.findOneAndDelete({ _id: id, userId: req.user.id }); + if (!task) { + return res.status(404).json({ errors: [{ msg: 'Task not found' }] }); + } + return res.json({ msg: 'Task deleted' }); + } catch (err) { + console.error('Delete task error:', err); + if (err.name === 'CastError') { + return res.status(400).json({ errors: [{ msg: 'Invalid task id' }] }); + } + return res.status(500).json({ errors: [{ msg: 'Server error' }] }); + } +}); + +module.exports = router; + + diff --git a/backend/server.js b/backend/server.js index 2c8683a..749be65 100644 --- a/backend/server.js +++ b/backend/server.js @@ -63,6 +63,7 @@ app.use("/api/github", githubRoute); app.use("/api/auth", authMiddleware, require("./routes/auth")); app.use("/api/profile", generalMiddleware, require("./routes/profile")); app.use("/api/contact", generalMiddleware, contactRouter); +app.use("/api/tasks", require("./routes/tasks.route")); // Default route diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 3012404..7aa514f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -23,6 +23,7 @@ import ProtectedRoute from "./Components/auth/ProtectedRoute"; import Dashboard from "./Components/Dashboard"; import FAQ from "./Components/FAQ"; import Pomodoro from "./Components/DashBoard/Pomodoro"; +import Todo from "./Components/DashBoard/Todo"; import { ArrowUp } from "lucide-react"; import GitHubProfile from "./Components/GitHubProfile"; import LeetCode from "./Components/DashBoard/LeetCode"; @@ -131,6 +132,7 @@ function App() { /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/Components/DashBoard/Todo.jsx b/frontend/src/Components/DashBoard/Todo.jsx new file mode 100644 index 0000000..11707ef --- /dev/null +++ b/frontend/src/Components/DashBoard/Todo.jsx @@ -0,0 +1,319 @@ +import React, { useEffect, useMemo, useState } from "react"; +import Topbar from "./Topbar"; +import BackButton from "../ui/backbutton"; +import { Plus, Pencil, Trash2, CheckCircle2, Circle, Filter } from "lucide-react"; + +// Mock-only UI for GSSOC To-Do page. Uses local state; backend wiring will follow. +export default function Todo() { + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(true); + const API = `${import.meta.env.VITE_API_URL}/api/tasks`; + + const [form, setForm] = useState({ title: "", description: "", deadline: "" }); + const [editingId, setEditingId] = useState(null); + const [filter, setFilter] = useState("all"); // all | upcoming | completed + const [showModal, setShowModal] = useState(false); + const [modalData, setModalData] = useState({ title: "", description: "", status: "pending", deadline: "" }); + + const pendingTasks = useMemo(() => tasks.filter(t => t.status === "pending"), [tasks]); + const completedTasks = useMemo(() => tasks.filter(t => t.status === "completed"), [tasks]); + + const filtered = (items) => { + if (filter === "completed") return items.filter(t => t.status === "completed"); + if (filter === "upcoming") return items.filter(t => t.status === "pending").sort((a,b)=> new Date(a.deadline) - new Date(b.deadline)); + return items; + }; + + // Load tasks on mount + useEffect(() => { + const token = localStorage.getItem("token"); + if (!token) { + setLoading(false); + return; + } + fetch(API, { headers: { "x-auth-token": token } }) + .then(async (res) => { + const data = await res.json(); + if (!res.ok) throw new Error(data?.errors?.[0]?.msg || "Failed to fetch tasks"); + setTasks(data); + }) + .catch((e) => console.error(e)) + .finally(() => setLoading(false)); + }, [API]); + + function resetForm() { + setForm({ title: "", description: "", deadline: "" }); + setModalData({ title: "", description: "", status: "pending", deadline: "" }); + setEditingId(null); + } + + function openAddModal() { + setModalData({ title: "", description: "", status: "pending", deadline: "" }); + setEditingId(null); + setShowModal(true); + } + + function handleSubmitModal(e) { + e.preventDefault(); + if (!modalData.title.trim()) return; + const token = localStorage.getItem("token"); + if (editingId) { + const taskId = editingId; + fetch(`${API}/${taskId}`, { + method: "PUT", + headers: { "Content-Type": "application/json", "x-auth-token": token }, + body: JSON.stringify({ + title: modalData.title, + description: modalData.description, + status: modalData.status, + deadline: modalData.deadline || null, + }) + }) + .then(async (res) => { + const data = await res.json(); + if (!res.ok) throw new Error(data?.errors?.[0]?.msg || "Failed to update task"); + setTasks(prev => prev.map(t => (t._id || t.id) === taskId ? data : t)); + setShowModal(false); + resetForm(); + }) + .catch((e) => console.error(e)); + } else { + fetch(API, { + method: "POST", + headers: { "Content-Type": "application/json", "x-auth-token": token }, + body: JSON.stringify({ + title: modalData.title, + description: modalData.description, + status: modalData.status, + deadline: modalData.deadline || null, + }) + }) + .then(async (res) => { + const data = await res.json(); + if (!res.ok) throw new Error(data?.errors?.[0]?.msg || "Failed to create task"); + setTasks(prev => [data, ...prev]); + setShowModal(false); + resetForm(); + }) + .catch((e) => console.error(e)); + } + } + + function toggleComplete(id) { + const token = localStorage.getItem("token"); + const task = tasks.find(t => t._id === id || t.id === id); + if (!task) return; + const newStatus = task.status === "pending" ? "completed" : "pending"; + const taskId = task._id || task.id; + fetch(`${API}/${taskId}`, { + method: "PUT", + headers: { "Content-Type": "application/json", "x-auth-token": token }, + body: JSON.stringify({ status: newStatus }) + }) + .then(async (res) => { + const data = await res.json(); + if (!res.ok) throw new Error(data?.errors?.[0]?.msg || "Failed to update task"); + setTasks(prev => prev.map(t => (t._id || t.id) === taskId ? data : t)); + }) + .catch((e) => console.error(e)); + } + + function removeTask(id) { + const token = localStorage.getItem("token"); + const task = tasks.find(t => t._id === id || t.id === id); + if (!task) return; + const taskId = task._id || task.id; + fetch(`${API}/${taskId}`, { method: "DELETE", headers: { "x-auth-token": token } }) + .then(async (res) => { + if (!res.ok) { + const data = await res.json(); + throw new Error(data?.errors?.[0]?.msg || "Failed to delete task"); + } + setTasks(prev => prev.filter(t => (t._id || t.id) !== taskId)); + }) + .catch((e) => console.error(e)); + } + + function startEdit(task) { + setEditingId(task._id || task.id); + setModalData({ + title: task.title, + description: task.description, + status: task.status, + deadline: task.deadline ? task.deadline.slice(0,16) : "", + }); + setShowModal(true); + } + + function GoalProgress() { + // simplistic mock: completed / total + const total = tasks.length || 1; + const done = completedTasks.length; + const percent = Math.round((done / total) * 100); + return ( +
+
+

Goals

+ {done}/{total} completed +
+
+
+
+

Track milestones like "Solve X LeetCode" or "Finish project".

+
+ ); + } + + const TaskCard = ({ task }) => ( +
+
+
+ +
+

{task.title}

+ {task.description &&

{task.description}

} + {task.deadline && ( +

Due {new Date(task.deadline).toLocaleString()}

+ )} +
+
+
+ + +
+
+
+ ); + + function WeeklyGoals() { + const weeklyTarget = 7; // mock + const weeklyDone = Math.min(weeklyTarget, completedTasks.length); + const percent = Math.round((weeklyDone / weeklyTarget) * 100); + return ( +
+
+

Weekly Goals

+ {weeklyDone}/{weeklyTarget} this week +
+
+
+
+

Set targets like "Solve 7 problems" weekly.

+
+ ); + } + + return ( +
+ +
+
+
+ +

To-Do List

+
+
+ + +
+ +
+
+ + {/* Add / Edit Modal */} + {showModal && ( +
+
+
+

{editingId ? "Edit Task" : "Add Task"}

+ +
+
+ setModalData({ ...modalData, title: e.target.value })} placeholder="Title" className="px-3 py-2 rounded-lg bg-[var(--background)] text-[var(--card-foreground)] border border-[var(--input)]" /> +