DEV Community

code lover
code lover

Posted on

todo

Backend

npm i bcryptjs cors dotenv express joi jsonwebtoken mongoose nodemon
folder structure

Image description

controllers

  • controllers/authController.js
const User = require("../models/User");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const { registerSchema, loginSchema } = require("../utils/authValidation");

const generateToken = (userId) => {
  return jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: "7d" });
};

const registerUser = async (req, res) => {
  const { error } = registerSchema.validate(req.body);
  if (error) return res.status(400).json({ msg: error.details[0].message });

  try {
    let user = await User.findOne({ email: req.body.email });
    if (user) return res.status(400).json({ msg: "User already exists" });

    const salt = await bcrypt.genSalt(10);
    const hashedPassword = await bcrypt.hash(req.body.password, salt);

    user = new User({ ...req.body, password: hashedPassword });
    await user.save();

    res.status(201).json({
      msg: "User registered successfully",
      token: generateToken(user.id),
      user: { id: user.id, name: user.name, email: user.email },
    });
  } catch (error) {
    res.status(500).json({ msg: "Server error" });
  }
};

const loginUser = async (req, res) => {
  const { error } = loginSchema.validate(req.body);
  if (error) return res.status(400).json({ msg: error.details[0].message });

  try {
    const user = await User.findOne({ email: req.body.email });
    if (!user) return res.status(400).json({ msg: "Invalid credentials" });

    const isMatch = await bcrypt.compare(req.body.password, user.password);
    if (!isMatch) return res.status(400).json({ msg: "Invalid credentials" });

    res.json({
      msg: "User login successfully",
      token: generateToken(user.id),
      user: { id: user.id, name: user.name, email: user.email },
    });
  } catch (error) {
    res.status(500).json({ msg: "Server error" });
  }
};

module.exports = { registerUser, loginUser };
Enter fullscreen mode Exit fullscreen mode

controllers/todoController.js

const Todo = require("../models/Todo");
const { todoSchema } = require("../utils/todoValidation");

const createTodo = async (req, res) => {
  const { error } = todoSchema.validate(req.body);
  if (error) return res.status(400).json({ msg: error.details[0].message });

  try {
    const todo = new Todo({ ...req.body, user: req.user });
    await todo.save();
    res.status(201).json(todo);
  } catch (error) {
    res.status(500).json({ msg: "Server error" });
  }
};

const getTodos = async (req, res) => {
  try {
    const todos = await Todo.find({ user: req.user }).sort({ createdAt: -1 });
    res.json(todos);
  } catch (error) {
    res.status(500).json({ msg: "Server error" });
  }
};

const updateTodo = async (req, res) => {
  const { error } = todoSchema.validate(req.body);
  if (error) return res.status(400).json({ msg: error.details[0].message });

  try {
    const todo = await Todo.findOneAndUpdate(
      { _id: req.params.id, user: req.user },
      req.body,
      { new: true }
    );
    if (!todo) return res.status(404).json({ msg: "Todo not found" });
    res.json(todo);
  } catch (error) {
    res.status(500).json({ msg: "Server error" });
  }
};

const deleteTodo = async (req, res) => {
  try {
    const todo = await Todo.findOneAndDelete({
      _id: req.params.id,
      user: req.user,
    });
    if (!todo) return res.status(404).json({ msg: "Todo not found" });
    res.json({ msg: "Todo deleted successfully" });
  } catch (error) {
    res.status(500).json({ msg: "Server error" });
  }
};

module.exports = { createTodo, getTodos, updateTodo, deleteTodo };
Enter fullscreen mode Exit fullscreen mode

middleware:-
middleware/authMiddleware.js

const jwt = require("jsonwebtoken");

const protect = (req, res, next) => {
  const authHeader = req.header("Authorization");

  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    return res.status(401).json({ msg: "No token, authorization denied" });
  }

  const token = authHeader.split(" ")[1]; // Extract token after "Bearer "

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded.userId;
    next();
  } catch (error) {
    res.status(401).json({ msg: "Invalid token" });
  }
};

module.exports = protect;
**models:-**
Enter fullscreen mode Exit fullscreen mode

models/Todo.js

const mongoose = require("mongoose");

const todoSchema = new mongoose.Schema(
  {
    user: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
    title: { type: String, required: true },
    description: { type: String },
    priority: {
      type: String,
      enum: ["low", "medium", "high"],
      default: "medium",
    },
    status: {
      type: String,
      enum: ["pending", "completed"],
      default: "pending",
    },
    dueDate: { type: Date, required: true },
  },
  { timestamps: true }
);

module.exports = mongoose.model("Todo", todoSchema);
Enter fullscreen mode Exit fullscreen mode

models/User.js

const mongoose = require("mongoose");

const userSchema = new mongoose.Schema(
  {
    name: { type: String, required: true },
    email: { type: String, required: true, unique: true },
    password: { type: String, required: true },
  },
  { timestamps: true }
);

module.exports = mongoose.model("User", userSchema);
Enter fullscreen mode Exit fullscreen mode

routes:-
routes/authRoutes.js

const express = require("express");
const { registerUser, loginUser } = require("../controllers/authController");

const router = express.Router();

router.post("/register", registerUser);

router.post("/login", loginUser);

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

routes/todoRoutes.js

const express = require("express");
const {
  createTodo,
  getTodos,
  updateTodo,
  deleteTodo,
} = require("../controllers/todoController");
const protect = require("../middleware/authMiddleware");

const router = express.Router();

router.post("/", protect, createTodo);
router.get("/", protect, getTodos);
router.put("/:id", protect, updateTodo);
router.delete("/:id", protect, deleteTodo);

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

utils:-
utils/authValidation.js

const Joi = require("joi");

const registerSchema = Joi.object({
  name: Joi.string().min(3).max(30).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(6).required(),
});

const loginSchema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(6).required(),
});

module.exports = { registerSchema, loginSchema };
Enter fullscreen mode Exit fullscreen mode

utils/jwt.js

const generateToken = (userId) => {
  return jwt.sign({ id: userId }, process.env.JWT_SECRET, {
    expiresIn: "1d",
  });
};

const verifyToken = (token) => {
  return jwt.verify(token, process.env.JWT_SECRET);
};
Enter fullscreen mode Exit fullscreen mode

utils/todoValidation.js

const Joi = require("joi");

const todoSchema = Joi.object({
  title: Joi.string().min(3).max(100).required(),
  description: Joi.string().max(500).allow(""),
  priority: Joi.string().valid("low", "medium", "high").default("medium"),
  status: Joi.string().valid("pending", "completed").default("pending"),
  dueDate: Joi.date().greater("now").required(),
});

module.exports = { todoSchema };
Enter fullscreen mode Exit fullscreen mode

.env

PORT=5000
MONGO_URI=
JWT_SECRET=i-am-utsav-ioopen-source
Enter fullscreen mode Exit fullscreen mode

config.js

const mongoose = require("mongoose");
require("dotenv").config();

const connectDB = async () => {
  try {
    await mongoose.connect(process.env.MONGO_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    console.log("MongoDB Connected...");
  } catch (error) {
    console.error("MongoDB Connection Failed:", error);
    process.exit(1);
  }
};

module.exports = connectDB;
Enter fullscreen mode Exit fullscreen mode

server.js

const express = require("express");
const dotenv = require("dotenv");
const cors = require("cors");
const connectDB = require("./config");

const authRoutes = require("./routes/authRoutes");
const todoRoutes = require("./routes/todoRoutes");

dotenv.config();
connectDB();

const app = express();

app.use(express.json());
app.use(cors());

app.use("/api/auth", authRoutes);
app.use("/api/todos", todoRoutes);

app.get("/", (req, res) => {
  res.send("API is running...");
});

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Enter fullscreen mode Exit fullscreen mode

package.json

{
  "name": "backend",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "bcryptjs": "^3.0.2",
    "cors": "^2.8.5",
    "dotenv": "^16.4.7",
    "express": "^4.21.2",
    "express-validator": "^7.2.1",
    "joi": "^17.13.3",
    "jsonwebtoken": "^9.0.2",
    "mongoose": "^8.10.1"
  },
  "devDependencies": {
    "nodemon": "^3.1.9"
  }
}
Enter fullscreen mode Exit fullscreen mode

Frontend

main.jsx

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.jsx";
// import AppRouter from "./AppRouter.jsx";
import { Toaster } from "react-hot-toast";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <App />
    {/* <AppRouter /> */}
    <Toaster position="top-right" />
  </StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

app.jsx

// import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
// import Login from "./pages/Login";
// import Register from "./pages/Register";
// import Dashboard from "./pages/Dashboard";
// import { ProtectedRoute } from "./authMiddleware";

import AuthProvider from "./provider/authProvider";
import Routes from "./routes";

function App() {
  return (
    // <Router>
    //   <Routes>
    //     <Route path="/" element={<Login />} />
    //     <Route path="/register" element={<Register />} />
    //     {/* <Route path="/dashboard" element={<Dashboard />} /> */}

    //     <Route
    //       path="/dashboard"
    //       element={
    //         <ProtectedRoute>
    //           <Dashboard />
    //         </ProtectedRoute>
    //       }
    //     />
    //   </Routes>
    // </Router>
    <AuthProvider>
      <Routes />
    </AuthProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

AppRouter.jsx

import {
  BrowserRouter as Router,
  Routes,
  Route,
  Navigate,
} from "react-router-dom";
import { useState, useEffect } from "react";
import Login from "./pages/Login";
import Register from "./pages/Register";
import Dashboard from "./pages/Dashboard";
// import NotFound from "./pages/NotFound";

const AppRouter = () => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  useEffect(() => {
    const token = localStorage.getItem("token");
    setIsAuthenticated(!!token);
  }, []);

  return (
    <Router>
      <Routes>
        <Route
          path="/"
          element={isAuthenticated ? <Dashboard /> : <Navigate to="/login" />}
        />
        <Route path="/login" element={<Login />} />
        <Route path="/register" element={<Register />} />
        {/* <Route path="*" element={<NotFound />} /> */}
      </Routes>
    </Router>
  );
};

export default AppRouter;
Enter fullscreen mode Exit fullscreen mode

authMiddleware.js

import { useNavigate } from "react-router-dom";

export const ProtectedRoute = ({ children }) => {
  const navigate = useNavigate();
  const token = localStorage.getItem("token");
  return token ? children : navigate("/");
};
Enter fullscreen mode Exit fullscreen mode

index.css


@tailwind utilities;

html.dark {
  background-color: #121212;
  color: white;
}

.dark .bg-white {
  background-color: #1e1e1e;
}

.dark .border {
  border-color: #333;
}

.dark .shadow {
  box-shadow: 0px 4px 6px rgba(255, 255, 255, 0.1);
}
Enter fullscreen mode Exit fullscreen mode

App.css

@import "tailwindcss";
Enter fullscreen mode Exit fullscreen mode

utils/axios.js

import axios from "axios";

const API = axios.create({
  baseURL: "http://localhost:5000/api",
});

API.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem("token");
    if (token) config.headers.Authorization = `Bearer ${token}`;
    return config;
  },
  (error) => Promise.reject(error)
);

export default API;
Enter fullscreen mode Exit fullscreen mode

routes
routes/ProtectedRoute.jsx

import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "../provider/authProvider";

export const ProtectedRoute = () => {
  const { token } = useAuth();

  // Check if the user is authenticated
  if (!token) {
    // If not authenticated, redirect to the login page
    return <Navigate to="/login" />;
  }

  // If authenticated, render the child routes
  return <Outlet />;
};
Enter fullscreen mode Exit fullscreen mode

routes/index.jsx

import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { useAuth } from "../provider/authProvider";
import { ProtectedRoute } from "./ProtectedRoute";
import Login from "../pages/Login";
// import Logout from "../pages/Logout";
import Dashboard from "../pages/Dashboard";
import TodoList from "../components/second/TodoList";

const Routes = () => {
  const { token } = useAuth();

  // Define public routes accessible to all users
  const routesForPublic = [
    {
      path: "/service",
      element: <div>Service Page</div>,
    },
    {
      path: "/about-us",
      element: <div>About Us</div>,
    },
  ];

  // Define routes accessible only to authenticated users
  const routesForAuthenticatedOnly = [
    {
      path: "/",
      element: <ProtectedRoute />, // Wrap the component in ProtectedRoute
      children: [
        {
          path: "",
          element: <Dashboard />,
          // element: <TodoList />,
        },
        {
          path: "/profile",
          element: <div>User Profile</div>,
        },
        // {
        //   path: "/logout",
        //   element: <Logout />,
        // },
      ],
    },
  ];

  // Define routes accessible only to non-authenticated users
  const routesForNotAuthenticatedOnly = [
    // {
    //   path: "/",
    //   element: <div>Home Page</div>,
    // },
    {
      path: "/login",
      element: <Login />,
    },
  ];

  // Combine and conditionally include routes based on authentication status
  const router = createBrowserRouter([
    ...routesForPublic,
    ...(!token ? routesForNotAuthenticatedOnly : []),
    ...routesForAuthenticatedOnly,
  ]);

  // Provide the router configuration using RouterProvider
  return <RouterProvider router={router} />;
};

export default Routes;
Enter fullscreen mode Exit fullscreen mode

provider
provider/authProvider.jsx

import axios from "axios";
import { createContext, useContext, useEffect, useMemo, useState } from "react";

const AuthContext = createContext();

const AuthProvider = ({ children }) => {
  // State to hold the authentication token
  const [token, setToken_] = useState(localStorage.getItem("token"));

  // Function to set the authentication token
  const setToken = (newToken) => {
    setToken_(newToken);
  };

  useEffect(() => {
    if (token) {
      axios.defaults.headers.common["Authorization"] = "Bearer " + token;
      localStorage.setItem("token", token);
    } else {
      delete axios.defaults.headers.common["Authorization"];
      localStorage.removeItem("token");
    }
  }, [token]);

  // Memoized value of the authentication context
  const contextValue = useMemo(
    () => ({
      token,
      setToken,
    }),
    [token]
  );

  // Provide the authentication context to the children components
  return (
    <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
  );
};

export const useAuth = () => {
  return useContext(AuthContext);
};

export default AuthProvider;
Enter fullscreen mode Exit fullscreen mode

pages
pages/Dashboard.jsx

import { useState, useEffect } from "react";
import API from "../utils/axios";
import TodoModal from "../components/TodoModal";
import toast from "react-hot-toast";
import DarkModeToggle from "../components/DarkModeToggle";
import { useAuth } from "../provider/authProvider";
import { useNavigate } from "react-router-dom";

const Dashboard = () => {
  const { setToken } = useAuth();
  const navigate = useNavigate();

  const [todos, setTodos] = useState([]);
  const [modalOpen, setModalOpen] = useState(false);
  const [editTodo, setEditTodo] = useState(null);
  const [statusFilter, setStatusFilter] = useState("all");
  const [sortOrder, setSortOrder] = useState("newest");
  const [filteredTodos, setFilteredTodos] = useState([]);
  const [loading, setLoading] = useState(false); // Add loading state

  const fetchTodos = async () => {
    setLoading(true); // Start loading
    try {
      const res = await API.get("/todos");
      setTodos(res.data);
    } catch (err) {
      console.error(err);
    } finally {
      setLoading(false); // End loading
    }
  };

  useEffect(() => {
    fetchTodos();
  }, []);

  useEffect(() => {
    let updatedTodos = [...todos];

    if (statusFilter !== "all") {
      updatedTodos = updatedTodos.filter((todo) =>
        statusFilter === "completed"
          ? todo.status === "completed"
          : todo.status === "pending"
      );
    }

    if (sortOrder === "newest") {
      updatedTodos.sort((a, b) => new Date(b.dueDate) - new Date(a.dueDate));
    } else {
      updatedTodos.sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate));
    }

    setFilteredTodos(updatedTodos);
  }, [statusFilter, sortOrder, todos]);

  const deleteTodo = async (id) => {
    try {
      await API.delete(`/todos/${id}`);
      setTodos((prev) => prev.filter((todo) => todo._id !== id));
      toast.success("To-Do deleted successfully!");
    } catch (err) {
      console.log(err);
      toast.error("Failed to delete To-Do!");
    }
  };

  // const logOut = () => {
  //   localStorage.removeItem("token");
  //   window.location.href = "/";
  // };



  const handleLogout = () => {
    setToken();
    navigate("/", { replace: true });
  };

  return (
    <div className="p-6">
      <DarkModeToggle />
      <button className="p-3 bg-gray-500 cursor-pointer" onClick={handleLogout}>
        Logout
      </button>{" "}
      <h2 className="text-xl font-bold mb-4">To-Do List</h2>
      <button
        className="p-2 bg-green-500 text-white rounded mb-4"
        onClick={() => setModalOpen(true)}
        aria-label="Add a new to-do"
      >
        Add To-Do
      </button>
      <div className="flex gap-4 mb-4">
        <select
          className="p-2 border rounded"
          onChange={(e) => setStatusFilter(e.target.value)}
          value={statusFilter}
        >
          <option value="all">All</option>
          <option value="completed">Completed</option>
          <option value="pending">Pending</option>
        </select>

        <select
          className="p-2 border rounded"
          onChange={(e) => setSortOrder(e.target.value)}
          value={sortOrder}
        >
          <option value="newest">Newest First</option>
          <option value="oldest">Oldest First</option>
        </select>

        <button
          className="p-2 bg-green-500 text-white rounded"
          onClick={() => setModalOpen(true)}
        >
          Add To-Do
        </button>
      </div>
      {
        loading ? (
          <p className="text-center">Loading todos...</p>
        ) : (
          <table className="w-full mt-4 border-collapse border border-gray-300">
            <thead>
              <tr className="bg-gray-200">
                <th scope="col" className="border p-2">Title</th>
                <th className="border p-2">Description</th>
                <th className="border p-2">Priority</th>
                <th className="border p-2">Due Date</th>
                <th className="border p-2">Status</th>
                <th className="border p-2">Actions</th>
              </tr>
            </thead>
            <tbody>
              {/* {todos.map((todo) => (
            <tr key={todo._id} className="border">
              <td className="border p-2">{todo.title}</td>
              <td className="border p-2">{todo.description}</td>
              <td className="border p-2">{todo.priority}</td>
              <td className="border p-2">
                {new Date(todo.dueDate).toLocaleDateString()}
              </td>
              <td className="border p-2">
                {todo.status === "completed" ? (
                  <span className="text-green-500">Completed</span>
                ) : (
                  <span className="text-red-500">Pending</span>
                )}
              </td>

              <td className="border p-2 flex gap-2">
                <button
                  className="p-1 bg-yellow-500 text-white rounded"
                  onClick={() => {
                    setEditTodo(todo);
                    setModalOpen(true);
                  }}
                >
                  Edit
                </button>
                <button
                  className="p-1 bg-red-500 text-white rounded"
                  onClick={() => deleteTodo(todo._id)}
                >
                  Delete
                </button>
              </td>
            </tr>
          ))} */}

              {filteredTodos.map((todo) => (
                <tr key={todo._id} className="border">
                  <td className="border p-2">{todo.title}</td>
                  <td className="border p-2">{todo.description}</td>
                  <td className="border p-2">{todo.priority}</td>
                  <td className="border p-2">
                    {new Date(todo.dueDate).toLocaleDateString()}
                  </td>
                  <td className="border p-2">
                    {todo.status === "completed" ? (
                      <span className="text-green-500">Completed</span>
                    ) : (
                      <span className="text-red-500">Pending</span>
                    )}
                  </td>
                  <td className="border p-2 flex gap-2">
                    <button
                      className="p-1 bg-yellow-500 text-white rounded"
                      onClick={() => {
                        setEditTodo(todo);
                        setModalOpen(true);
                      }}
                    >
                      Edit
                    </button>
                    <button
                      className="p-1 bg-red-500 text-white rounded"
                      onClick={() => deleteTodo(todo._id)}
                    >
                      Delete
                    </button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      <TodoModal
        isOpen={modalOpen}
        onClose={() => {
          setModalOpen(false);
          setEditTodo(null);
        }}
        refreshTodos={fetchTodos}
        editTodo={editTodo}
      />
    </div>
  );
};

export default Dashboard;
Enter fullscreen mode Exit fullscreen mode

pages/Login.jsx

import { useNavigate } from "react-router-dom";
import toast from "react-hot-toast";
import { Formik } from "formik";
import * as Yup from "yup";
import API from "../utils/axios";
import { useAuth } from "../provider/authProvider";

const LoginInitialValues = {
  email: "",
  password: "",
};

const LogInSchema = Yup.object().shape({
  email: Yup.string()
    .email("Invalid email address")
    .required("Email is required"),
  password: Yup.string()
    .required("Password is required")
    .matches(
      /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/,
      "Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one special character"
    ),
});

const Login = () => {
  const navigate = useNavigate();
  const { setToken } = useAuth();

  // const handleLogin = async (e) => {
  //   e.preventDefault();
  //   try {
  //     const { data } = await axios.post(
  //       "http://localhost:5000/api/auth/login",
  //       { email, password }
  //     );
  //     localStorage.setItem("token", data.token);
  //     toast.success("Login successful!");
  //     navigate("/dashboard");
  //   } catch (error) {
  //     toast.error(error.response?.data?.msg || "Login failed");
  //   }
  // };

  const handleSubmit = async (values, { setSubmitting }) => {
    try {
      const response = await API.post("/auth/login", {
        email: values.email,
        password: values.password,
      });
      console.log("LLLL", response);

      // localStorage.setItem("token", response.data.token);
      toast.success(response?.data?.data?.msg || "Login successfully");
      // navigate("/dashboard");
      setToken(response?.data?.token);
      navigate("/", { replace: true });
    } catch (error) {
      // console.log("error.....", error);
      toast.error(error?.response?.data?.msg || "Login failed");
    }
    setSubmitting(false);
  };

  return (
    <div className="flex justify-center items-center h-screen">
      {/* <form
        onSubmit={handleLogin}
        className="bg-white p-6 rounded-lg shadow-lg"
      > */}
      <Formik
        initialValues={LoginInitialValues}
        validationSchema={LogInSchema}
        onSubmit={(values, { setSubmitting }) =>
          handleSubmit(values, setSubmitting)
        }
      >
        {({
          values,
          errors,
          touched,
          handleChange,
          handleBlur,
          handleSubmit,
          isSubmitting,
          isValid,
        }) => (
          <form className="space-y-2 max-w-2xl w-full" onSubmit={handleSubmit}>
            <h2 className="text-2xl font-bold mb-4">Login</h2>
            <input
              type="text"
              placeholder="Email Address"
              className={`border p-2 w-full ${errors.email && touched.email ? "border-red-600" : ""
                }`}
              name="email"
              onChange={handleChange}
              onBlur={handleBlur}
              value={values.email}
            />
            <div className="text-red-500 text-sm">
              {errors.email && touched.email && errors.email}
            </div>
            <input
              type="password"
              placeholder="Password"
              className={`border p-2 w-full ${errors.password && touched.password ? "border-red-600" : ""
                }`}
              name="password"
              onChange={handleChange}
              onBlur={handleBlur}
              value={values.password}
            />
            <div className="text-red-500 text-sm">
              {errors.password && touched.password && errors.password}
            </div>
            <button
              type="submit"
              className="bg-blue-500 text-white p-2 rounded w-full"
            >
              Login
            </button>
            <div
              className="mx-auto text-center mt-2 cursor-pointer"
              onClick={() => navigate("/register")}
            >
              Register
            </div>
            {/* </form> */}
          </form>
        )}
      </Formik>
    </div>
  );
};

export default Login;
Enter fullscreen mode Exit fullscreen mode

pages/Register.jsx

import { useNavigate } from "react-router-dom";
import toast from "react-hot-toast";
import { Formik } from "formik";
import * as Yup from "yup";
import API from "../utils/axios";

const LoginInitialValues = {
  name: "",
  email: "",
  password: "",
  confirmPassword: "",
};

const LogInSchema = Yup.object().shape({
  name: Yup.string().required("Username is required"),
  email: Yup.string()
    .email("Invalid email address")
    .required("Email is required"),
  password: Yup.string()
    .required("Password is required")
    .matches(
      /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/,
      "Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one special character"
    ),
  confirmPassword: Yup.string()
    .oneOf([Yup.ref("password")], "Passwords must match")
    .min(8, "Confirm password must be at least 8 characters")
    .required("Confirm password is required"),
});

const Register = () => {
  const navigator = useNavigate();
  const handleSubmit = async (values, { setSubmitting }) => {
    try {
      const response = await API.post("/auth/register", {
        name: values.name,
        email: values.email,
        password: values.password,
      });
      toast.success(response?.data?.data?.msg || "Registration successfully");
      navigator("/");
    } catch (error) {
      console.log("error.....", error);
      toast.error(error?.response?.data?.msg || "Registration failed");
    }
    setSubmitting(false);
  };
  return (
    <div className="flex justify-center items-center h-screen">
      {/* <form
        onSubmit={handleRegister}
        className="bg-white p-6 rounded-lg shadow-lg"
      > */}
      <Formik
        initialValues={LoginInitialValues}
        validationSchema={LogInSchema}
        onSubmit={(values, { setSubmitting }) =>
          handleSubmit(values, setSubmitting)
        }
      >
        {({
          values,
          errors,
          touched,
          handleChange,
          handleBlur,
          handleSubmit,
          isSubmitting,
          isValid,
        }) => (
          <form className="space-y-2 max-w-2xl w-full" onSubmit={handleSubmit}>
            <h2 className="text-2xl font-bold">Register</h2>
            <input
              type="text"
              placeholder="Name"
              className={`border p-2 w-full ${
                errors.name && touched.name ? "border-red-600" : ""
              }`}
              name="name"
              onChange={handleChange}
              onBlur={handleBlur}
              value={values.name}
            />
            <div className="text-red-500 text-sm">
              {errors.name && touched.name && errors.name}
            </div>
            <input
              type="text"
              placeholder="Email Address"
              className={`border p-2 w-full ${
                errors.email && touched.email ? "border-red-600" : ""
              }`}
              name="email"
              onChange={handleChange}
              onBlur={handleBlur}
              value={values.email}
            />
            <div className="text-red-500 text-sm">
              {errors.email && touched.email && errors.email}
            </div>
            <input
              type="text"
              placeholder="Password"
              className={`border p-2 w-full ${
                errors.password && touched.password ? "border-red-600" : ""
              }`}
              name="password"
              onChange={handleChange}
              onBlur={handleBlur}
              value={values.password}
            />
            <div className="text-red-500 text-sm">
              {errors.password && touched.password && errors.password}
            </div>
            <input
              type="password"
              placeholder="confirmPassword"
              className={`border p-2 w-full ${
                errors.confirmPassword && touched.confirmPassword
                  ? "border-red-600"
                  : ""
              }`}
              name="confirmPassword"
              onChange={handleChange}
              onBlur={handleBlur}
              value={values.confirmPassword}
            />
            <div className="text-red-500 text-sm">
              {errors.confirmPassword &&
                touched.confirmPassword &&
                errors.confirmPassword}
            </div>
            <button
              type="submit"
              className="bg-green-500 text-white p-2 rounded w-full"
            >
              Register
            </button>
            <div
              className="mx-auto text-center mt-2 cursor-pointer"
              onClick={() => navigator("/")}
            >
              Login
            </div>
          </form>
        )}
      </Formik>
    </div>
  );
};

export default Register;
Enter fullscreen mode Exit fullscreen mode

components
components/DarkModeToggle.jsx

import { useState, useEffect } from "react";

const DarkModeToggle = () => {
  const [darkMode, setDarkMode] = useState(
    localStorage.getItem("theme") === "dark"
  );

  useEffect(() => {
    if (darkMode) {
      document.documentElement.classList.add("dark");
      localStorage.setItem("theme", "dark");
    } else {
      document.documentElement.classList.remove("dark");
      localStorage.setItem("theme", "light");
    }
  }, [darkMode]);

  return (
    <button
      onClick={() => setDarkMode(!darkMode)}
      className="p-2 bg-gray-500 text-white rounded"
    >
      {darkMode ? "Light Mode" : "Dark Mode"}
    </button>
  );
};

export default DarkModeToggle;
Enter fullscreen mode Exit fullscreen mode

components/TodoModal.jsx

import { useEffect } from "react";
import { useFormik } from "formik";
import * as Yup from "yup";
import API from "../utils/axios";
import toast from "react-hot-toast";

const TodoModal = ({ isOpen, onClose, refreshTodos, editTodo }) => {
  const formik = useFormik({
    initialValues: {
      title: editTodo ? editTodo.title : "",
      description: editTodo ? editTodo.description : "",
      priority: editTodo ? editTodo.priority : "low",
      dueDate: editTodo ? editTodo.dueDate.split("T")[0] : "",
      status: editTodo ? editTodo.status : "pending",
    },
    validationSchema: Yup.object({
      title: Yup.string().required("Title is required"),
      priority: Yup.string().oneOf(["low", "medium", "high"]).required(),
      dueDate: Yup.date().required("Due date is required"),
      status: Yup.string().oneOf(["pending", "completed"]).required(),
    }),
    onSubmit: async (values) => {
      try {
        if (editTodo) {
          await API.put(`/todos/${editTodo._id}`, values);
          toast.success("To-Do updated successfully!");
        } else {
          await API.post("/todos", values);
          toast.success("To-Do added successfully!");
        }
        refreshTodos();
        onClose();
      } catch (err) {
        toast.error("Something went wrong!");
      }
    },
  });

  useEffect(() => {
    if (editTodo) {
      formik.setValues({
        title: editTodo.title,
        priority: editTodo.priority,
        dueDate: editTodo.dueDate.split("T")[0],
        status: editTodo.status,
        description: editTodo.description,
      });
    }
  }, [editTodo]);

  return (
    isOpen && (
      <div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
        <div className="bg-white p-6 rounded-lg w-96">
          <h2 className="text-xl font-bold mb-4">
            {editTodo ? "Edit" : "Add"} To-Do
          </h2>
          <form onSubmit={formik.handleSubmit}>
            <input
              type="text"
              name="title"
              placeholder="Title"
              className="w-full p-2 border rounded mb-2"
              {...formik.getFieldProps("title")}
            />
            {formik.touched.title && formik.errors.title && (
              <p className="text-red-500">{formik.errors.title}</p>
            )}
            <input
              type="text"
              name="description"
              placeholder="description"
              className="w-full p-2 border rounded mb-2"
              {...formik.getFieldProps("description")}
            />
            {formik.touched.description && formik.errors.description && (
              <p className="text-red-500">{formik.errors.description}</p>
            )}

            <select
              name="priority"
              className="w-full p-2 border rounded mb-2"
              {...formik.getFieldProps("priority")}
            >
              <option value="low">Low</option>
              <option value="medium">Medium</option>
              <option value="high">High</option>
            </select>

            <select
              name="status"
              className="w-full p-2 border rounded mb-2"
              {...formik.getFieldProps("status")}
            >
              <option value="pending">Pending</option>
              <option value="completed">Completed</option>
            </select>

            <input
              type="date"
              name="dueDate"
              className="w-full p-2 border rounded mb-2"
              {...formik.getFieldProps("dueDate")}
            />
            {formik.touched.dueDate && formik.errors.dueDate && (
              <p className="text-red-500">{formik.errors.dueDate}</p>
            )}

            <button
              type="submit"
              className="w-full p-2 bg-blue-500 text-white rounded"
            >
              {editTodo ? "Update" : "Add"} To-Do
            </button>
          </form>
        </div>
      </div>
    )
  );
};

export default TodoModal;
Enter fullscreen mode Exit fullscreen mode

components/second/TodoForm.jsx

// TodoForm.jsx
import React from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
import API from '../../utils/axios';

const TodoForm = ({ fetchTodos, todo = {}, isUpdate = false, closeModal }) => {
    const validationSchema = Yup.object({
        title: Yup.string()
            .min(2, 'Title must be at least 2 characters')
            .max(50, 'Title must be less than 50 characters')
            .required('Title is required'),
        description: Yup.string()
            .max(200, 'Description must be less than 200 characters')
            .required('Description is required'),
        dueDate: Yup.date()
            .required('Due date is required')
            .min(new Date(), 'Due date cannot be in the past'),
        priority: Yup.string()
            .oneOf(['low', 'medium', 'high'], 'Invalid priority')
            .required('Priority is required'),
        status: Yup.string().oneOf(["pending", "completed"])
            .required('Status is required'),
    });

    const initialValues = {
        title: todo.title || '',
        description: todo.description || '',
        dueDate: todo.dueDate ? new Date(todo.dueDate).toISOString().split('T')[0] : '',
        priority: todo.priority || 'low',
        status: todo.status || 'pending',
    };

    const handleSubmit = async (values, { resetForm }) => {
        try {
            if (isUpdate) {
                await API.put(`/todos/${todo._id}`, values);
                closeModal();
            } else {
                await API.post('/todos', values);
                resetForm();
            }
            fetchTodos();
        } catch (error) {
            console.error('Error submitting todo:', error);
        }
    };

    return (
        <Formik
            initialValues={initialValues}
            validationSchema={validationSchema}
            onSubmit={handleSubmit}
            enableReinitialize
        >
            {({ isSubmitting }) => (
                <Form className="space-y-4">
                    <div>
                        <label htmlFor="title" className="block text-sm font-medium text-gray-700">Title</label>
                        <Field
                            name="title"
                            type="text"
                            className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
                        />
                        <ErrorMessage name="title" component="div" className="text-red-500 text-sm mt-1" />
                    </div>

                    <div>
                        <label htmlFor="description" className="block text-sm font-medium text-gray-700">Description</label>
                        <Field
                            name="description"
                            as="textarea"
                            className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
                            rows="3"
                        />
                        <ErrorMessage name="description" component="div" className="text-red-500 text-sm mt-1" />
                    </div>

                    <div>
                        <label htmlFor="dueDate" className="block text-sm font-medium text-gray-700">Due Date</label>
                        <Field
                            name="dueDate"
                            type="date"
                            className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
                        />
                        <ErrorMessage name="dueDate" component="div" className="text-red-500 text-sm mt-1" />
                    </div>

                    <div>
                        <label htmlFor="priority" className="block text-sm font-medium text-gray-700">Priority</label>
                        <Field
                            as="select"
                            name="priority"
                            className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
                        >
                            <option value="low">Low</option>
                            <option value="medium">Medium</option>
                            <option value="high">High</option>
                        </Field>
                        <ErrorMessage name="priority" component="div" className="text-red-500 text-sm mt-1" />
                    </div>

                    <div>
                        <label htmlFor="status" className="block text-sm font-medium text-gray-700">Status</label>
                        <Field
                            as="select"
                            name="status"
                            className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
                        >
                            <option value="pending">Pending</option>
                            <option value="completed">Completed</option>
                        </Field>
                        <ErrorMessage name="status" component="div" className="text-red-500 text-sm mt-1" />
                    </div>

                    <button
                        type="submit"
                        disabled={isSubmitting}
                        className="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 disabled:bg-indigo-400"
                    >
                        {isUpdate ? 'Update' : 'Create'} Todo
                    </button>
                </Form>
            )}
        </Formik>
    );
};

export default TodoForm;
Enter fullscreen mode Exit fullscreen mode

components/second/TodoList.jsx

// TodoList.jsx
import React, { useState, useEffect } from 'react';
import API from '../../utils/axios';
import TodoForm from './TodoForm';
import { useAuth } from '../../provider/authProvider';
import { useNavigate } from 'react-router-dom';

const TodoList = () => {
    const { setToken } = useAuth();
    const navigate = useNavigate();



    const [todos, setTodos] = useState([]);
    const [showUpdateModal, setShowUpdateModal] = useState(false);
    const [showDeleteModal, setShowDeleteModal] = useState(false);
    const [selectedTodo, setSelectedTodo] = useState(null);

    useEffect(() => {
        fetchTodos();
    }, []);

    const fetchTodos = async () => {
        try {
            const response = await API.get('/todos');
            setTodos(response.data);
        } catch (error) {
            console.error('Error fetching todos:', error);
        }
    };

    const handleDeleteClick = (todo) => {
        setSelectedTodo(todo);
        setShowDeleteModal(true);
    };

    const handleUpdateClick = (todo) => {
        setSelectedTodo(todo);
        setShowUpdateModal(true);
    };

    const handleDelete = async () => {
        try {
            await API.delete(`/todos/${selectedTodo._id}`);
            setTodos(todos.filter(todo => todo._id !== selectedTodo._id));
            setShowDeleteModal(false);
        } catch (error) {
            console.error('Error deleting todo:', error);
        }
    };

    const handleLogout = () => {
        setToken();
        navigate("/", { replace: true });
    };

    return (
        <div className="container mx-auto mt-8 px-4">
            <button className='p-2 bg-gray-600 text-white cursor-pointer' onClick={handleLogout}>Logout</button>
            <h2 className="text-2xl font-bold mb-4">Todo List</h2>
            <TodoForm fetchTodos={fetchTodos} />

            <div className="mt-6 overflow-x-auto">
                <table className="w-full border-collapse">
                    <thead>
                        <tr className="bg-gray-100">
                            <th className="p-3 text-left">Title</th>
                            <th className="p-3 text-left">Description</th>
                            <th className="p-3 text-left">Due Date</th>
                            <th className="p-3 text-left">Priority</th>
                            <th className="p-3 text-left">Status</th>
                            <th className="p-3 text-left">Actions</th>
                        </tr>
                    </thead>
                    <tbody>
                        {todos.map(todo => (
                            <tr key={todo._id} className="border-b hover:bg-gray-50">
                                <td className="p-3">{todo.title}</td>
                                <td className="p-3">{todo.description}</td>
                                <td className="p-3">{new Date(todo.dueDate).toLocaleDateString()}</td>
                                <td className="p-3">
                                    <span className={`px-2 py-1 rounded capitalize ${todo.priority === 'high' ? 'bg-red-100 text-red-800' :
                                        todo.priority === 'medium' ? 'bg-yellow-100 text-yellow-800' :
                                            'bg-green-100 text-green-800'
                                        }`}>
                                        {todo.priority}
                                    </span>
                                </td>
                                <td className="p-3">
                                    <span className={`px-2 py-1 rounded capitalize ${todo.status === 'completed' ? 'bg-green-100 text-green-800' :
                                        todo.status === 'in-progress' ? 'bg-blue-100 text-blue-800' :
                                            'bg-gray-100 text-gray-800'
                                        }`}>
                                        {todo.status}
                                    </span>
                                </td>
                                <td className="p-3">
                                    <button
                                        onClick={() => handleUpdateClick(todo)}
                                        className="bg-yellow-500 text-white px-3 py-1 rounded mr-2 hover:bg-yellow-600"
                                    >
                                        Edit
                                    </button>
                                    <button
                                        onClick={() => handleDeleteClick(todo)}
                                        className="bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600"
                                    >
                                        Delete
                                    </button>
                                </td>
                            </tr>
                        ))}
                    </tbody>
                </table>
            </div>

            {showUpdateModal && (
                <div className="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center">
                    <div className="bg-white p-6 rounded-lg w-full max-w-md">
                        <div className="flex justify-between items-center mb-4">
                            <h3 className="text-xl font-bold">Update Todo</h3>
                            <button onClick={() => setShowUpdateModal(false)} className="text-gray-500 hover:text-gray-700">
                                ×
                            </button>
                        </div>
                        <TodoForm
                            fetchTodos={fetchTodos}
                            todo={selectedTodo}
                            isUpdate={true}
                            closeModal={() => setShowUpdateModal(false)}
                        />
                    </div>
                </div>
            )}

            {showDeleteModal && (
                <div className="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center">
                    <div className="bg-white p-6 rounded-lg w-full max-w-md">
                        <h3 className="text-xl font-bold mb-4">Confirm Delete</h3>
                        <p className="mb-4">Are you sure you want to delete "{selectedTodo?.title}"?</p>
                        <div className="flex justify-end gap-2">
                            <button
                                onClick={() => setShowDeleteModal(false)}
                                className="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"
                            >
                                Cancel
                            </button>
                            <button
                                onClick={handleDelete}
                                className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
                            >
                                Delete
                            </button>
                        </div>
                    </div>
                </div>
            )}
        </div>
    );
};

export default TodoList;
Enter fullscreen mode Exit fullscreen mode

Top comments (0)