diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..b338f14 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,34 @@ +name: Build on PR + +on: + pull_request: + branches: + - '*' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Install Dependencies in Frontend + run: | + cd frontend + npm install + + - name: Install Dependencies in Backend + run: | + cd backend + npm install + + - name: Run Frontend Build + run: | + cd frontend + npm run build + + diff --git a/backend/config.js b/backend/config.js index 45fac5b..04163bf 100644 --- a/backend/config.js +++ b/backend/config.js @@ -1,3 +1,3 @@ -const JWT_SECRET = "IAMGAYLOL"; - -module.exports = JWT_SECRET; +const JWT_SECRET = "IAMGAYLOL"; + +module.exports = JWT_SECRET; diff --git a/backend/db.js b/backend/db.js index 1763e05..7c29252 100644 --- a/backend/db.js +++ b/backend/db.js @@ -1,29 +1,29 @@ -const mongoose = require("mongoose"); -mongoose.connect("mongodb://localhost:27017/basicTodoApp"); - -const UserSchema = mongoose.Schema({ - firstName: String, - lastName: String, - username: String, - password: String, - todos: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: "Todo", - }, - ], -}); - -const TodoSchema = mongoose.Schema({ - title: String, - description: String, - completed: Boolean, -}); - -const User = mongoose.model("User", UserSchema); -const Todo = mongoose.model("Todo", TodoSchema); - -module.exports = { - User, - Todo, -}; +const mongoose = require("mongoose"); +mongoose.connect("mongodb://localhost:27017/basicTodoApp"); + +const UserSchema = mongoose.Schema({ + firstName: String, + lastName: String, + username: String, + password: String, + todos: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: "Todo", + }, + ], +}); + +const TodoSchema = mongoose.Schema({ + title: String, + description: String, + completed: Boolean, +}); + +const User = mongoose.model("User", UserSchema); +const Todo = mongoose.model("Todo", TodoSchema); + +module.exports = { + User, + Todo, +}; diff --git a/backend/index.js b/backend/index.js index 254aaa9..fbeb24c 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,18 +1,18 @@ -const express = require("express"); -const PORT = 3000; -const bodyParser = require("body-parser"); -const userRouter = require("./routes/user"); -const cors = require("cors"); - -const app = express(); -app.use(cors()); -app.use(bodyParser.json()); -app.use("/user", userRouter); - -app.get("/", (req, res) => { - res.send("Hello World! Welcome to Todo"); -}); - -app.listen(PORT, () => { - console.log(`Server is running on port ${PORT}`); -}); +const express = require("express"); +const PORT = 3000; +const bodyParser = require("body-parser"); +const userRouter = require("./routes/user"); +const cors = require("cors"); + +const app = express(); +app.use(cors()); +app.use(bodyParser.json()); +app.use("/user", userRouter); + +app.get("/", (req, res) => { + res.send("Hello World! Welcome to Todo"); +}); + +app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); +}); diff --git a/backend/middleware/authMiddleware.js b/backend/middleware/authMiddleware.js index 3c432c4..18d6f4d 100644 --- a/backend/middleware/authMiddleware.js +++ b/backend/middleware/authMiddleware.js @@ -1,43 +1,43 @@ -const jwt = require("jsonwebtoken"); -const JWT_SECRET = require("../config"); - -const authMiddleware = (req, res, next) => { - const authHeader = req.headers.authorization; - - if (!authHeader) { - return res.status(401).json({ - message: "No Authorization Header Found", - }); - } - - const words = authHeader.split(" "); - - if (words[0] !== "Bearer" || words.length !== 2) { - return res.status(401).json({ - message: "Invalid Token Format", - }); - } - - const token = words[1]; - - try { - const decoded = jwt.verify(token, JWT_SECRET); - - if (decoded.userId) { - req.userId = decoded.userId; - next(); - } else { - res.status(401).json({ - message: "Invalid Token", - }); - } - } catch (error) { - console.error("The Error is " + error); - res.status(500).json({ - message: "Some Error Occurred", - error: error.message, - }); - } -}; - -module.exports = authMiddleware; // Ensure this is correct +const jwt = require("jsonwebtoken"); +const JWT_SECRET = require("../config"); + +const authMiddleware = (req, res, next) => { + const authHeader = req.headers.authorization; + + if (!authHeader) { + return res.status(401).json({ + message: "No Authorization Header Found", + }); + } + + const words = authHeader.split(" "); + + if (words[0] !== "Bearer" || words.length !== 2) { + return res.status(401).json({ + message: "Invalid Token Format", + }); + } + + const token = words[1]; + + try { + const decoded = jwt.verify(token, JWT_SECRET); + + if (decoded.userId) { + req.userId = decoded.userId; + next(); + } else { + res.status(401).json({ + message: "Invalid Token", + }); + } + } catch (error) { + console.error("The Error is " + error); + res.status(500).json({ + message: "Some Error Occurred", + error: error.message, + }); + } +}; + +module.exports = authMiddleware; // Ensure this is correct diff --git a/backend/middleware/validateMiddleware.js b/backend/middleware/validateMiddleware.js index 0da043b..3854d39 100644 --- a/backend/middleware/validateMiddleware.js +++ b/backend/middleware/validateMiddleware.js @@ -1,48 +1,48 @@ -const zod = require("zod"); -const { default: errorMap } = require("zod/locales/en.js"); - -const validateMiddleware = (schema) => (req, res, next) => { - try { - const result = schema.safeParse(req.body); - if (!result.success) { - return res.status(400).json({ - message: "Validation Error", - errors: result.error.errors.map((err) => ({ - path: err.path.join("."), // Path of the field with the error - message: err.message, // Error message - })), - }); - } - next(); - } catch (error) { - return res.status(500).json({ - message: "Internal Server Error", - error: error.message, - }); - } -}; - -const userSchema = zod.object({ - firstName: zod.string().min(1).max(30), // Should not be empty - lastName: zod.string().min(1).max(30), // Should not be empty - username: zod.string().email().min(1), // Should not be empty and must be a valid email - password: zod.string().min(8).max(20), // Length constraints -}); - -const signinSchema = zod.object({ - username: zod.string().email(), - password: zod.string().min(8).max(20), -}); - -const todoSchema = zod.object({ - title: zod.string().min(1, "Title is required").max(30), - description: zod.string().optional(), - completed: zod.boolean(), -}); - -module.exports = { - userSchema, - todoSchema, - signinSchema, - validateMiddleware, -}; +const zod = require("zod"); +const { default: errorMap } = require("zod/locales/en.js"); + +const validateMiddleware = (schema) => (req, res, next) => { + try { + const result = schema.safeParse(req.body); + if (!result.success) { + return res.status(400).json({ + message: "Validation Error", + errors: result.error.errors.map((err) => ({ + path: err.path.join("."), // Path of the field with the error + message: err.message, // Error message + })), + }); + } + next(); + } catch (error) { + return res.status(500).json({ + message: "Internal Server Error", + error: error.message, + }); + } +}; + +const userSchema = zod.object({ + firstName: zod.string().min(1).max(30), // Should not be empty + lastName: zod.string().min(1).max(30), // Should not be empty + username: zod.string().email().min(1), // Should not be empty and must be a valid email + password: zod.string().min(8).max(20), // Length constraints +}); + +const signinSchema = zod.object({ + username: zod.string().email(), + password: zod.string().min(8).max(20), +}); + +const todoSchema = zod.object({ + title: zod.string().min(1, "Title is required").max(30), + description: zod.string().optional(), + completed: zod.boolean().optional(), +}); + +module.exports = { + userSchema, + todoSchema, + signinSchema, + validateMiddleware, +}; diff --git a/backend/routes/user.js b/backend/routes/user.js index a48e670..6b36b6e 100644 --- a/backend/routes/user.js +++ b/backend/routes/user.js @@ -1,209 +1,215 @@ -const { Router } = require("express"); -const router = Router(); -const { - validateMiddleware, - userSchema, - todoSchema, - signinSchema, -} = require("../middleware/validateMiddleware"); -const authMiddleware = require("../middleware/authMiddleware"); -const jwt = require("jsonwebtoken"); -const { User, Todo } = require("../db"); -const JWT_SECRET = require("../config"); - -// Signup Route -router.post("/signup", validateMiddleware(userSchema), async (req, res) => { - const { firstName, lastName, username, password } = req.body; - try { - const response = await User.findOne({ username }); - if (response) { - return res.status(400).json({ - message: "User Already Exists", - }); - } - - const newUser = await User.create({ - firstName, - lastName, - username, - password, - }); - - const token = jwt.sign({ userId: newUser._id }, JWT_SECRET, {}); - - return res.status(200).json({ - message: "User Created Successfully", - success: true, - token: token, - }); - } catch (error) { - console.error("Error during signup:", error); // Log the error details - return res.status(500).json({ - message: "Some Error Occurred", - success: false, - error: error.message, // Include error message in response for debugging - }); - } -}); - -// Signin Route -router.post("/login", validateMiddleware(signinSchema), async (req, res) => { - const { username, password } = req.body; - - try { - const response = await User.findOne({ username, password }); - if (!response) { - return res.status(400).json({ - message: "No User Found with the credentials", - success: false, - }); - } - - const token = jwt.sign({ userId: response._id }, JWT_SECRET, {}); - - return res.status(200).json({ - message: "User Signed In Successfully", - success: true, - token: token, - }); - } catch (error) { - return res.status(500).json({ - message: "Some Error Occurred", - }); - } -}); - -// Get Todos Route -router.get("/todos", authMiddleware, async (req, res) => { - const userId = req.userId; - - try { - const user = await User.findById(userId).populate("todos"); // Use userId here - - if (!user) { - return res.status(400).json({ - message: "No User found with todos", - }); - } - - return res.status(200).json({ - todos: user.todos, - }); - } catch (error) { - console.error("The error is " + error); - return res.status(500).json({ - message: "Some Error Occurred", - error: error.message, - }); - } -}); - -// Add Todo Route -router.post( - "/addTodo", - authMiddleware, - validateMiddleware(todoSchema), - async (req, res) => { - const userId = req.userId; // Corrected to match the field set by authMiddleware - const { title, description, completed } = req.body; - - try { - const newTodo = await Todo.create({ - title, - description, - completed, - }); - - const user = await User.findByIdAndUpdate( - userId, - { $push: { todos: newTodo._id } }, - { new: true } - ); - - if (!user) { - return res.status(404).json({ - message: "User not found", - }); - } - - // Return a success response - return res.status(201).json({ - message: "Todo added successfully", - todo: newTodo, - }); - } catch (error) { - return res.status(500).json({ - message: "Some Error Occurred", - }); - } - } -); - -// Update Todo Route -router.put( - "/updateTodo", - authMiddleware, - validateMiddleware(todoSchema), - async (req, res) => { - const { id, title, description, completed } = req.body; - - try { - // Create an update object based on which fields are present - const updateData = {}; - if (title !== undefined) updateData.title = title; - if (description !== undefined) updateData.description = description; - if (completed !== undefined) updateData.completed = completed; - - const updatedTodo = await Todo.findByIdAndUpdate(id, updateData, { - new: true, - }); - - if (!updatedTodo) { - return res.status(404).json({ error: "Todo not found" }); - } - - res.status(200).json(updatedTodo); - } catch (error) { - res.status(500).json({ error: "Failed to update todo" }); - } - } -); - -router.put("/completeTodo/:id", authMiddleware, async (req, res) => { - const { id } = req.params; // Now getting id from the URL params - - try { - const updatedTodo = await Todo.findByIdAndUpdate( - id, - { completed: true }, - { new: true } - ); - - if (!updatedTodo) { - return res.status(404).json({ error: "Todo not found" }); - } - - res.status(200).json(updatedTodo); - } catch (error) { - res.status(500).json({ error: "Failed to complete todo" }); - } -}); - -// Delete Todo Route -router.delete("/delete/:id", authMiddleware, async (req, res) => { - const { id } = req.params; // Extract the todo id from the request body - - try { - const deletedTodo = await Todo.findByIdAndDelete(id); // Find and delete the todo by id - - if (!deletedTodo) { - return res.status(404).json({ error: "Todo not found" }); - } - - res.status(200).json({ message: "Todo deleted successfully", deletedTodo }); - } catch (error) { - res.status(500).json({ error: "Failed to delete todo" }); - } -}); - -module.exports = router; +const { Router } = require("express"); +const router = Router(); +const { + validateMiddleware, + userSchema, + todoSchema, + signinSchema, +} = require("../middleware/validateMiddleware"); +const authMiddleware = require("../middleware/authMiddleware"); +const jwt = require("jsonwebtoken"); +const { User, Todo } = require("../db"); +const JWT_SECRET = require("../config"); + +// Signup Route +router.post("/signup", validateMiddleware(userSchema), async (req, res) => { + const { firstName, lastName, username, password } = req.body; + try { + const response = await User.findOne({ username }); + if (response) { + return res.status(400).json({ + message: "User Already Exists", + }); + } + + const newUser = await User.create({ + firstName, + lastName, + username, + password, + }); + + const token = jwt.sign({ userId: newUser._id }, JWT_SECRET, { + expiresIn: "1h", + }); + + return res.status(200).json({ + message: "User Created Successfully", + success: true, + token: token, + }); + } catch (error) { + console.error("Error during signup:", error); // Log the error details + return res.status(500).json({ + message: "Some Error Occurred", + success: false, + error: error.message, // Include error message in response for debugging + }); + } +}); + +// Signin Route +router.post("/login", validateMiddleware(signinSchema), async (req, res) => { + const { username, password } = req.body; + + try { + const response = await User.findOne({ username, password }); + if (!response) { + return res.status(400).json({ + message: "No User Found with the credentials", + success: false, + }); + } + + const token = jwt.sign({ userId: response._id }, JWT_SECRET, { + expiresIn: "1h", + }); + + return res.status(200).json({ + message: "User Signed In Successfully", + success: true, + token: token, + }); + } catch (error) { + return res.status(500).json({ + message: "Some Error Occurred", + }); + } +}); + +// Get Todos Route +router.get("/todos", authMiddleware, async (req, res) => { + const userId = req.userId; + + try { + const user = await User.findById(userId).populate("todos"); // Use userId here + + if (!user) { + return res.status(400).json({ + message: "No User found with todos", + }); + } + + return res.status(200).json({ + todos: user.todos, + }); + } catch (error) { + console.error("The error is " + error); + return res.status(500).json({ + message: "Some Error Occurred", + error: error.message, + }); + } +}); + +// Add Todo Route +router.post( + "/addTodo", + authMiddleware, + validateMiddleware(todoSchema), + async (req, res) => { + const userId = req.userId; // Corrected to match the field set by authMiddleware + const { title, description, completed } = req.body; + + try { + const newTodo = await Todo.create({ + title, + description, + completed, + }); + + const user = await User.findByIdAndUpdate( + userId, + { $push: { todos: newTodo._id } }, + { new: true } + ); + + if (!user) { + return res.status(404).json({ + message: "User not found", + }); + } + + // Return a success response + return res.status(201).json({ + message: "Todo added successfully", + todo: newTodo, + }); + } catch (error) { + return res.status(500).json({ + message: "Some Error Occurred", + }); + } + } +); + +// Update Todo Route +router.put( + "/updateTodo/:id", + authMiddleware, + validateMiddleware(todoSchema), + async (req, res) => { + const { title, description, completed } = req.body; + const { id } = req.params; + + try { + // Create an update object based on which fields are present + const updateData = {}; + if (title !== undefined) updateData.title = title; + if (description !== undefined) updateData.description = description; + if (completed !== undefined) updateData.completed = completed; + + const updatedTodo = await Todo.findByIdAndUpdate(id, updateData, { + new: true, + }); + + if (!updatedTodo) { + return res.status(404).json({ error: "Todo not found" }); + } + + res.status(200).json(updatedTodo); + } catch (error) { + res.status(500).json({ error: "Failed to update todo" }); + } + } +); + +router.put("/completeTodo/:id", authMiddleware, async (req, res) => { + const { id } = req.params; + const { completed } = req.body; // Get the completed state from the request body + + try { + const updatedTodo = await Todo.findByIdAndUpdate( + id, + { completed }, // Use the completed state from the request + { new: true } // Return the updated document + ); + + if (!updatedTodo) { + return res.status(404).json({ error: "Todo not found" }); + } + + res.status(200).json(updatedTodo); + } catch (error) { + res.status(500).json({ error: "Failed to update todo" }); + } +}); + +// Delete Todo Route +router.delete("/delete/:id", authMiddleware, async (req, res) => { + const { id } = req.params; // Extract the todo id from the request body + + try { + const deletedTodo = await Todo.findByIdAndDelete(id); // Find and delete the todo by id + + if (!deletedTodo) { + return res.status(404).json({ error: "Todo not found" }); + } + + res.status(200).json({ message: "Todo deleted successfully", deletedTodo }); + } catch (error) { + res.status(500).json({ error: "Failed to delete todo" }); + } +}); + +module.exports = router; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index dc5b560..624de73 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -6,26 +6,23 @@ import { Login } from "./components/Login"; import { AppBar } from "./components/AppBar"; import { Todos } from "./components/Todos"; import { NewHome } from "./components/NewHome"; +import { TodoRender } from "./context"; function App() { - console.log("App Rerenders"); - const token = localStorage.getItem("token"); return ( - - - } /> - } /> - } /> - {token ? ( - } /> - ) : ( - } /> - )} - } /> - + + + + } /> + } /> + } /> + : } /> + } /> + + ); } diff --git a/frontend/src/components/AddTodo.jsx b/frontend/src/components/AddTodo.jsx index 6207c22..dbfb521 100644 --- a/frontend/src/components/AddTodo.jsx +++ b/frontend/src/components/AddTodo.jsx @@ -1,19 +1,35 @@ -import { useState } from "react"; -import { Fields } from "./Fields"; - -export function AddTodo() { - const [showFields, setShowFields] = useState(false); - - const toggleFields = () => { - setShowFields(true); // Toggle visibility of Fields - }; - - return ( -
- - {showFields && } {/* Conditionally render Fields */} -
- ); -} +import { useState, useContext } from "react"; +import { Fields } from "./Fields"; +import { TodoContext } from "../context"; + +export function AddTodo() { + const [showFields, setShowFields] = useState(false); + const { setRender } = useContext(TodoContext); // Get setRender from context + + const toggleFields = () => { + setShowFields(!showFields); + }; + + const handleTodoAdded = () => { + setShowFields(false); // Hide the form after adding a todo + setRender((prev) => !prev); // Re-fetch todos after adding a new one + }; + + return ( +
+ + {showFields && ( +
+ +
+ )} +
+ ); +} diff --git a/frontend/src/components/AppBar.jsx b/frontend/src/components/AppBar.jsx index 1f985f1..6ac18e7 100644 --- a/frontend/src/components/AppBar.jsx +++ b/frontend/src/components/AppBar.jsx @@ -1,33 +1,49 @@ -import { Router, useNavigate } from "react-router-dom"; -import { Logout } from "./Logout"; -import { Todos } from "./Todos"; - -export function AppBar() { - const navigate = useNavigate(); - const token = localStorage.getItem("token"); - - return ( -
- - {token ? ( - - ) : null} - {!token ? ( -
- - -
- ) : null} - - {token ? : null} -
- ); -} +import { useNavigate } from "react-router-dom"; +import { Logout } from "./Logout"; + +export function AppBar() { + const navigate = useNavigate(); + const token = localStorage.getItem("token"); + + return ( +
+
+ + {token ? ( + + ) : null} +
+ + {!token ? ( +
+ + +
+ ) : null} + + {token ? : null} +
+ ); +} diff --git a/frontend/src/components/CompleteButton.jsx b/frontend/src/components/CompleteButton.jsx index 35132b0..3d512ee 100644 --- a/frontend/src/components/CompleteButton.jsx +++ b/frontend/src/components/CompleteButton.jsx @@ -1,29 +1,42 @@ -import axios from "axios"; - -// CompleteButton component to mark a todo as complete -export function CompleteButton({ todoId }) { - const onClick = async () => { - try { - // Send todoId in the URL as a parameter - await axios.put( - `http://localhost:3000/user/completeTodo/${todoId}`, // Pass todoId in the URL - {}, - { - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + localStorage.getItem("token"), - }, - } - ); - console.log("Todo Completed"); - } catch (error) { - console.log("Error While Completing Todo: " + error); - } - }; - - return ( - - ); -} +import axios from "axios"; +import { useContext } from "react"; +import { TodoContext } from "../context"; + +// CompleteButton component to mark a todo as complete +export function CompleteButton({ todoId, isCompleted }) { + const { render, setRender } = useContext(TodoContext); + + const onClick = async () => { + try { + await axios.put( + `http://localhost:3000/user/completeTodo/${todoId}`, + { completed: !isCompleted }, + { + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + localStorage.getItem("token"), + }, + } + ); + console.log( + isCompleted ? "Todo marked as incomplete" : "Todo marked as complete" + ); + setRender((prev) => !prev); + } catch (error) { + console.log("Error While Completing Todo: " + error); + } + }; + + return ( + + ); +} diff --git a/frontend/src/components/DeleteButton.jsx b/frontend/src/components/DeleteButton.jsx index 224fb91..39cd61e 100644 --- a/frontend/src/components/DeleteButton.jsx +++ b/frontend/src/components/DeleteButton.jsx @@ -1,22 +1,31 @@ -import axios from "axios"; - -export function DeleteButton({ todoId }) { - const onDelete = async () => { - try { - await axios.delete(`http://localhost:3000/user/delete/${todoId}`, { - headers: { - Authorization: "Bearer " + localStorage.getItem("token"), - }, - }); - } catch (error) { - console.error("Some Error Occured " + error); - } - }; - return ( -
- -
- ); -} +import axios from "axios"; +import { useContext } from "react"; +import { TodoContext } from "../context"; + +export function DeleteButton({ todoId }) { + const { render, setRender } = useContext(TodoContext); + + const onDelete = async () => { + try { + await axios.delete(`http://localhost:3000/user/delete/${todoId}`, { + headers: { + Authorization: "Bearer " + localStorage.getItem("token"), + }, + }); + setRender((prev) => !prev); + } catch (error) { + console.error("Some Error Occurred " + error); + } + }; + + return ( +
+ +
+ ); +} diff --git a/frontend/src/components/EditTodo.jsx b/frontend/src/components/EditTodo.jsx index 8b13789..d12e7f9 100644 --- a/frontend/src/components/EditTodo.jsx +++ b/frontend/src/components/EditTodo.jsx @@ -1 +1,102 @@ - +import { useState } from "react"; +import axios from "axios"; + +export function EditTodo({ todo, onEditComplete }) { + const [editMode, setEditMode] = useState(false); + const [editTitle, setEditTitle] = useState(todo.title); + const [editDescription, setEditDescription] = useState(todo.description); + const [isCompleted, setIsCompleted] = useState(todo.completed); // Track completed state + + const handleEditClick = () => { + setEditMode(true); // Enable editing mode + }; + + const handleCancelClick = () => { + setEditMode(false); // Disable editing mode + setEditTitle(todo.title); // Reset title to original + setEditDescription(todo.description); // Reset description to original + setIsCompleted(todo.completed); // Reset completed state to original + }; + + const handleSaveClick = async () => { + try { + await axios.put( + `http://localhost:3000/user/updateTodo/${todo._id}`, + { + title: editTitle, + description: editDescription, + completed: isCompleted, // Include completed state in the update + }, + { + headers: { + Authorization: "Bearer " + localStorage.getItem("token"), + }, + } + ); + setEditMode(false); // Disable editing mode after save + onEditComplete(); // Trigger re-render of todos after saving + } catch (error) { + console.error("Error saving todo:", error); + } + }; + + return ( +
+
+ setEditTitle(e.target.value)} + className={`border ${ + editMode ? "border-blue-300" : "border-gray-300" + } rounded p-3 w-full mb-3 transition duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500`} + /> + setEditDescription(e.target.value)} + className={`border ${ + editMode ? "border-blue-300" : "border-gray-300" + } rounded p-3 w-full mb-3 transition duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500`} + /> + {editMode && ( // Only show the checkbox when in edit mode +
+ setIsCompleted(e.target.checked)} // Update completed state + disabled={!editMode} // Disable checkbox when not in edit mode + className="mr-2 h-5 w-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500" + /> + +
+ )} +
+ {editMode ? ( + <> + + + + ) : ( + + )} +
+
+
+ ); +} diff --git a/frontend/src/components/Fields.jsx b/frontend/src/components/Fields.jsx index 4d22a97..aa9495f 100644 --- a/frontend/src/components/Fields.jsx +++ b/frontend/src/components/Fields.jsx @@ -1,45 +1,78 @@ -import { useState } from "react"; -import axios from "axios"; - -export function Fields() { - const [title, setTitle] = useState(""); - const [description, setDescription] = useState(""); - - const clickHere = async () => { - try { - const response = await axios.post( - "http://localhost:3000/user/addTodo", - { - title: title, - description: description, - completed: false, - }, - { - headers: { - Authorization: "Bearer " + localStorage.getItem("token"), - }, - } - ); - } catch (error) { - console.error(error); - } - }; - - return ( -
- setTitle(e.target.value)} - /> - setDescription(e.target.value)} - /> - -
- ); -} +import { useState } from "react"; +import axios from "axios"; + +export function Fields({ onTodoAdded }) { + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [error, setError] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + + const clickHere = async () => { + setError(null); + setSuccessMessage(null); + + if (!title || !description) { + setError("Please fill in both fields."); + return; + } + + try { + await axios.post( + "http://localhost:3000/user/addTodo", + { + title: title, + description: description, + completed: false, + }, + { + headers: { + Authorization: "Bearer " + localStorage.getItem("token"), + }, + } + ); + + // Clear the fields after successful submission + setTitle(""); + setDescription(""); + setSuccessMessage("Todo added successfully!"); + + // Notify parent to refresh the list and hide form + onTodoAdded(); + } catch (error) { + console.error(error); + setError("Error adding todo, please try again."); + } + }; + + return ( +
+

Add a New Todo

+ setTitle(e.target.value)} + className="border border-gray-300 rounded p-3 w-full mb-3 focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + setDescription(e.target.value)} + className="border border-gray-300 rounded p-3 w-full mb-4 focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + + + {/* Show success or error messages */} + {error &&

{error}

} + {successMessage && ( +

{successMessage}

+ )} +
+ ); +} diff --git a/frontend/src/components/Landing.jsx b/frontend/src/components/Landing.jsx index 0bcb458..952cf74 100644 --- a/frontend/src/components/Landing.jsx +++ b/frontend/src/components/Landing.jsx @@ -1,24 +1,24 @@ -import { useNavigate } from "react-router-dom"; - -export function Landing() { - const navigate = useNavigate(); - - return ( -
-
-

- Welcome to Our App -

-

- The best place to manage your tasks and improve your productivity. -

- -
-
- ); -} +import { useNavigate } from "react-router-dom"; + +export function Landing() { + const navigate = useNavigate(); + + return ( +
+
+

+ Welcome to TaskNest +

+

+ The best place to manage your tasks and improve your productivity. +

+ +
+
+ ); +} diff --git a/frontend/src/components/Login.jsx b/frontend/src/components/Login.jsx index a96f57d..da9b021 100644 --- a/frontend/src/components/Login.jsx +++ b/frontend/src/components/Login.jsx @@ -1,98 +1,98 @@ -import { useState } from "react"; -import { useNavigate } from "react-router-dom"; -import axios from "axios"; - -export function Login() { - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [error, setError] = useState(""); - const navigate = useNavigate(); - - const handleSubmit = async (e) => { - e.preventDefault(); - console.log(username); - console.log(password); - - try { - const result = await axios.post("http://localhost:3000/user/login", { - username, - password, - }); - - console.log(result); - - if (result.data.success) { - localStorage.setItem("token", result.data.token); - window.alert(result.data.message); - navigate("/todos"); - // Change Later - } else { - setError(result.data.message); - } - } catch (error) { - // Safely access errors array, if it exists - const errors = error.response?.data?.errors; - - if (Array.isArray(errors) && errors.length > 0) { - const [{ message }] = errors; // Destructure first error message - window.alert(message || "Login failed"); - setError(message || "An error occurred"); - } else { - const fallbackMessage = - error.response?.data?.message || "An unknown error occurred"; - window.alert(fallbackMessage); - setError(fallbackMessage); - } - - console.error("Error during login:", error.response?.data); - } finally { - setUsername(""); - setPassword(""); - } - }; - - return ( -
-
-

Log In

- {error &&
{error}
} -
- setUsername(e.target.value)} - className="w-full p-3 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-blue-500" - /> -
-
- setPassword(e.target.value)} - className="w-full p-3 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-blue-500" - /> -
- -
- Don't Have an Account?{" "} - navigate("/signup")} - > - Sign Up - -
-
-
- ); -} +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import axios from "axios"; + +export function Login() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const navigate = useNavigate(); + + const handleSubmit = async (e) => { + e.preventDefault(); + console.log(username); + console.log(password); + + try { + const result = await axios.post("http://localhost:3000/user/login", { + username, + password, + }); + + console.log(result); + + if (result.data.success) { + localStorage.setItem("token", result.data.token); + window.alert(result.data.message); + navigate("/todos"); + // Change Later + } else { + setError(result.data.message); + } + } catch (error) { + // Safely access errors array, if it exists + const errors = error.response?.data?.errors; + + if (Array.isArray(errors) && errors.length > 0) { + const [{ message }] = errors; + window.alert(message || "Login failed"); + setError(message || "An error occurred"); + } else { + const fallbackMessage = + error.response?.data?.message || "An unknown error occurred"; + window.alert(fallbackMessage); + setError(fallbackMessage); + } + + console.error("Error during login:", error.response?.data); + } finally { + setUsername(""); + setPassword(""); + } + }; + + return ( +
+
+

Log In

+ {error &&
{error}
} +
+ setUsername(e.target.value)} + className="w-full p-3 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-blue-500" + /> +
+
+ setPassword(e.target.value)} + className="w-full p-3 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-blue-500" + /> +
+ +
+ Don't Have an Account?{" "} + navigate("/signup")} + > + Sign Up + +
+
+
+ ); +} diff --git a/frontend/src/components/Logout.jsx b/frontend/src/components/Logout.jsx index ef6dbd3..aa80b01 100644 --- a/frontend/src/components/Logout.jsx +++ b/frontend/src/components/Logout.jsx @@ -1,14 +1,25 @@ -import { useNavigate } from "react-router-dom"; - -export function Logout() { - const navigate = useNavigate(); - const onLogout = () => { - localStorage.removeItem("token"); - navigate("/"); - }; - return ( -
- -
- ); -} +import { useContext } from "react"; +import { useNavigate } from "react-router-dom"; +import { TodoContext } from "../context"; + +export function Logout() { + const navigate = useNavigate(); + const { setRender } = useContext(TodoContext); + + const onLogout = () => { + localStorage.removeItem("token"); + setRender((prev) => !prev); + navigate("/"); + }; + + return ( +
+ +
+ ); +} diff --git a/frontend/src/components/NewHome.jsx b/frontend/src/components/NewHome.jsx index a51bffd..f980a08 100644 --- a/frontend/src/components/NewHome.jsx +++ b/frontend/src/components/NewHome.jsx @@ -1,7 +1,7 @@ -export function NewHome() { - return ( -
-

Welcome to my home page

-
- ); -} +export function NewHome() { + return ( +
+

Welcome to TaskNest

+
+ ); +} diff --git a/frontend/src/components/Signup.jsx b/frontend/src/components/Signup.jsx index 3ffef13..8aa62fe 100644 --- a/frontend/src/components/Signup.jsx +++ b/frontend/src/components/Signup.jsx @@ -1,100 +1,100 @@ -import { useState } from "react"; -import axios from "axios"; -import { useNavigate } from "react-router-dom"; - -export function Signup() { - const navigate = useNavigate(); - const [firstName, setFirstName] = useState(""); - const [lastName, setLastName] = useState(""); - const [username, setUserName] = useState(""); - const [password, setPassword] = useState(""); - const [error, setError] = useState(""); // State to handle errors - - const handleSubmit = async (e) => { - e.preventDefault(); // Prevent page reload on form submission - setError(""); // Reset error state - - try { - const result = await axios.post("http://localhost:3000/user/signup", { - firstName, - lastName, - username, - password, - }); - - // console.log(result); - - if (result.data.success) { - localStorage.setItem("token", result.data.token); - navigate("/"); - } else { - setError("Signup failed"); - } - - // Reset input fields - setFirstName(""); - setLastName(""); - setUserName(""); - setPassword(""); - } catch (error) { - window.alert(error.response?.data.message || "Signup failed"); - console.error("Error during signup:", error.response?.data); - setError(error.response?.data.message || "An error occurred"); - } - }; - - return ( -
-
-

Sign Up

- {error &&
{error}
} - {/* Error message display */} -
- setFirstName(e.target.value)} - className="w-full p-3 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-blue-500" - /> -
-
- setLastName(e.target.value)} - className="w-full p-3 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-blue-500" - /> -
-
- setUserName(e.target.value)} - className="w-full p-3 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-blue-500" - /> -
-
- setPassword(e.target.value)} - className="w-full p-3 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-blue-500" - /> -
- -
-
- ); -} +import { useState } from "react"; +import axios from "axios"; +import { useNavigate } from "react-router-dom"; + +export function Signup() { + const navigate = useNavigate(); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [username, setUserName] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); // State to handle errors + + const handleSubmit = async (e) => { + e.preventDefault(); // Prevent page reload on form submission + setError(""); // Reset error state + + try { + const result = await axios.post("http://localhost:3000/user/signup", { + firstName, + lastName, + username, + password, + }); + + // console.log(result); + + if (result.data.success) { + localStorage.setItem("token", result.data.token); + navigate("/"); + } else { + setError("Signup failed"); + } + + // Reset input fields + setFirstName(""); + setLastName(""); + setUserName(""); + setPassword(""); + } catch (error) { + window.alert(error.response?.data.message || "Signup failed"); + console.error("Error during signup:", error.response?.data); + setError(error.response?.data.message || "An error occurred"); + } + }; + + return ( +
+
+

Sign Up

+ {error &&
{error}
} + {/* Error message display */} +
+ setFirstName(e.target.value)} + className="w-full p-3 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-blue-500" + /> +
+
+ setLastName(e.target.value)} + className="w-full p-3 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-blue-500" + /> +
+
+ setUserName(e.target.value)} + className="w-full p-3 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-blue-500" + /> +
+
+ setPassword(e.target.value)} + className="w-full p-3 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-blue-500" + /> +
+ +
+
+ ); +} diff --git a/frontend/src/components/Todos.jsx b/frontend/src/components/Todos.jsx index 526169b..57c5364 100644 --- a/frontend/src/components/Todos.jsx +++ b/frontend/src/components/Todos.jsx @@ -1,66 +1,64 @@ -import { useEffect, useState } from "react"; -import axios from "axios"; -import { CompleteButton } from "./CompleteButton"; -import { DeleteButton } from "./DeleteButton"; -import { AddTodo } from "./AddTodo"; -export function Todos() { - const [todos, setTodos] = useState([]); - - useEffect(() => { - const fetchTodos = async () => { - try { - const result = await axios.get("http://localhost:3000/user/todos", { - headers: { - Authorization: "Bearer " + localStorage.getItem("token"), - }, - }); - - setTodos(result.data.todos); - } catch (error) { - console.error("Error fetching todos:", error); - } - }; - - fetchTodos(); - }, [todos]); // Run only once on mount - - const addTodo = (newTodo) => { - setTodos((prevTodos) => [...prevTodos, newTodo]); // Update state with the new todo - }; - - return ( -
-
-

- Your Todos -

- -
- -
- ); -} +import { useContext, useEffect, useState } from "react"; +import { TodoContext } from "../context"; +import { CompleteButton } from "./CompleteButton"; +import { DeleteButton } from "./DeleteButton"; +import axios from "axios"; +import { AddTodo } from "./AddTodo"; +import { EditTodo } from "./EditTodo"; + +export function Todos() { + const { render, setRender } = useContext(TodoContext); + const [todos, setTodos] = useState([]); + + useEffect(() => { + const fetchTodos = async () => { + try { + const response = await axios.get("http://localhost:3000/user/todos", { + headers: { + Authorization: "Bearer " + localStorage.getItem("token"), + }, + }); + setTodos(response.data.todos); + } catch (error) { + console.error("Error fetching todos:", error); + } + }; + fetchTodos(); + }, [render]); + + const handleEditComplete = () => { + setRender((prev) => !prev); // Trigger re-fetching todos on edit completion + }; + + return ( +
+

+ Your Todos +

+
+ {todos.map((todo) => ( +
+
+ +
+ {todo.completed ? "Completed" : "Not Completed"} +
+
+
+ + +
+
+ ))} +
+ +
+ ); +} diff --git a/frontend/src/context.jsx b/frontend/src/context.jsx new file mode 100644 index 0000000..63b4601 --- /dev/null +++ b/frontend/src/context.jsx @@ -0,0 +1,13 @@ +import { createContext, useState } from "react"; + +export const TodoContext = createContext(); + +export function TodoRender({ children }) { + const [render, setRender] = useState(false); + + return ( + + {children} + + ); +}