Task Management System is one of the most important tools when you want to organize your tasks. NodeJS and ExpressJS are used in this article to create a REST API for performing all CRUD operations on task. It has two models User and Task. ReactJS and Tailwind CSS are used to create a frontend interface part in which we can add, delete, and update tasks.
Output Preview: Let us have a look at how the final output will look like
Prerequisites
Approach to create Task Management System:
Write the Approach(flow of the app) in bullets points.
- First of all we will create server for the task management application.
- In the server part we will implement API for performing operations in the task management application.
- After that we will implement the frontend part .
- Then we will run the frontend application as well as the server part.
Steps to Create the task management system:
Step 1: Create the folder for the project:
mkdir task-manager
cd task-manager
Step 2: Create the server by using the following commands.
mkdir server
cd server
npm init -y
Step 3: Install the required dependencies:
npm i express mongoose nodemon bcrypt dotenv cors jsonwebtoken
Folder Structure(backend):
Dependencies(backend): The updated dependencies in package.json file for backend will look like.
"dependencies": {
"bcrypt": "^5.0.1",
"cors": "^2.8.5",
"dotenv": "^16.0.0",
"express": "^4.17.3",
"jsonwebtoken": "^8.5.1",
"mongoose": "^6.2.3"
},
"devDependencies": {
"nodemon": "^2.0.22"
}
Step 4: Create an .env file and store the following in it.
PORT = 8000
MONGODB_URL = mongodb://localhost:27017
ACCESS_TOKEN_SECRET = ENTERTEXTHERE
Step 5: Now add the following code in the respective files
//app.js const express = require( "express" );
const app = express(); const mongoose = require( "mongoose" );
const path = require( "path" );
const cors = require( "cors" );
require( "dotenv" ).config();
// routes const authRoutes = require( "./routes/authRoutes" );
const taskRoutes = require( "./routes/taskRoutes" );
const profileRoutes = require( "./routes/profileRoutes" );
app.use(express.json()); app.use(cors()); const mongoUrl = process.env.MONGODB_URL; mongoose.connect(mongoUrl, (err) => { if (err) throw err;
console.log( "Mongodb connected..." );
}); app.use( "/api/auth" , authRoutes);
app.use( "/api/tasks" , taskRoutes);
app.use( "/api/profile" , profileRoutes);
if (process.env.NODE_ENV === "production" ) {
app.use(express.static(path.resolve(__dirname, "../frontend/build" )));
app.get( "*" , (req, res) =>
res.sendFile(path.resolve(__dirname, "../frontend/build/index.html" ))
);
} const port = process.env.PORT || 5000; app.listen(port, () => { console.log(`Backend is running on port ${port}`);
}); |
//controllers/authControllers.js const User = require( "../models/User" );
const bcrypt = require( "bcrypt" );
const { createAccessToken } = require( "../utils/token" );
const { validateEmail } = require( "../utils/validation" );
exports.signup = async (req, res) => { try {
const { name, email, password } = req.body;
if (!name || !email || !password) {
return res.status(400).json({ msg: "Please fill all the fields" });
}
if ( typeof name !== "string" || typeof email !== "string" || typeof password !== "string" ) {
return res.status(400).json({ msg: "Please send string values only" });
}
if (password.length < 4) {
return res.status(400).json({ msg: "Password length must be atleast 4 characters" });
}
if (!validateEmail(email)) {
return res.status(400).json({ msg: "Invalid Email" });
}
const user = await User.findOne({ email });
if (user) {
return res.status(400).json({ msg: "This email is already registered" });
}
const hashedPassword = await bcrypt.hash(password, 10);
await User.create({ name, email, password: hashedPassword });
res.status(200).json({ msg: "Congratulations!! Account has been created for you.." });
}
catch (err) {
console.error(err);
return res.status(500).json({ msg: "Internal Server Error" });
}
} exports.login = async (req, res) => { try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ status: false , msg: "Please enter all details!!" });
}
const user = await User.findOne({ email });
if (!user) return res.status(400).json({ status: false , msg: "This email is not registered!!" });
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) return res.status(400).json({ status: false , msg: "Password incorrect!!" });
const token = createAccessToken({ id: user._id });
delete user.password;
res.status(200).json({ token, user, status: true , msg: "Login successful.." });
}
catch (err) {
console.error(err);
return res.status(500).json({ status: false , msg: "Internal Server Error" });
}
} |
//controllers/profileControllers.js const User = require( "../models/User" );
exports.getProfile = async (req, res) => { try {
const user = await User.findById(req.user.id).select( "-password" );
res.status(200).json({ user, status: true , msg: "Profile found successfully.." });
}
catch (err) {
console.error(err);
return res.status(500).json({ status: false , msg: "Internal Server Error" });
}
} |
//controllers/taskControllers.js const Task = require( "../models/Task" );
const { validateObjectId } = require( "../utils/validation" );
exports.getTasks = async (req, res) => { try {
const tasks = await Task.find({ user: req.user.id });
res.status(200).json({ tasks, status: true , msg: "Tasks found successfully.." });
}
catch (err) {
console.error(err);
return res.status(500).json({ status: false , msg: "Internal Server Error" });
}
} exports.getTask = async (req, res) => { try {
if (!validateObjectId(req.params.taskId)) {
return res.status(400).json({ status: false , msg: "Task id not valid" });
}
const task = await Task.findOne({ user: req.user.id, _id: req.params.taskId });
if (!task) {
return res.status(400).json({ status: false , msg: "No task found.." });
}
res.status(200).json({ task, status: true , msg: "Task found successfully.." });
}
catch (err) {
console.error(err);
return res.status(500).json({ status: false , msg: "Internal Server Error" });
}
} exports.postTask = async (req, res) => { try {
const { description } = req.body;
if (!description) {
return res.status(400).json({ status: false , msg: "Description of task not found" });
}
const task = await Task.create({ user: req.user.id, description });
res.status(200).json({ task, status: true , msg: "Task created successfully.." });
}
catch (err) {
console.error(err);
return res.status(500).json({ status: false , msg: "Internal Server Error" });
}
} exports.putTask = async (req, res) => { try {
const { description } = req.body;
if (!description) {
return res.status(400).json({ status: false , msg: "Description of task not found" });
}
if (!validateObjectId(req.params.taskId)) {
return res.status(400).json({ status: false , msg: "Task id not valid" });
}
let task = await Task.findById(req.params.taskId);
if (!task) {
return res.status(400).json({ status: false , msg: "Task with given id not found" });
}
if (task.user != req.user.id) {
return res.status(403).json({ status: false , msg: "You can't update task of another user" });
}
task = await Task.findByIdAndUpdate(req.params.taskId, { description }, { new : true });
res.status(200).json({ task, status: true , msg: "Task updated successfully.." });
}
catch (err) {
console.error(err);
return res.status(500).json({ status: false , msg: "Internal Server Error" });
}
} exports.deleteTask = async (req, res) => { try {
if (!validateObjectId(req.params.taskId)) {
return res.status(400).json({ status: false , msg: "Task id not valid" });
}
let task = await Task.findById(req.params.taskId);
if (!task) {
return res.status(400).json({ status: false , msg: "Task with given id not found" });
}
if (task.user != req.user.id) {
return res.status(403).json({ status: false , msg: "You can't delete task of another user" });
}
await Task.findByIdAndDelete(req.params.taskId);
res.status(200).json({ status: true , msg: "Task deleted successfully.." });
}
catch (err) {
console.error(err);
return res.status(500).json({ status: false , msg: "Internal Server Error" });
}
} |
//middlewares.js/index.js const jwt = require( "jsonwebtoken" );
const User = require( "../models/User" );
const { ACCESS_TOKEN_SECRET } = process.env; exports.verifyAccessToken = async (req, res, next) => { const token = req.header( "Authorization" );
if (!token) return res.status(400).json({ status: false , msg: "Token not found" });
let user;
try {
user = jwt.verify(token, ACCESS_TOKEN_SECRET);
}
catch (err) {
return res.status(401).json({ status: false , msg: "Invalid token" });
}
try {
user = await User.findById(user.id);
if (!user) {
return res.status(401).json({ status: false , msg: "User not found" });
}
req.user = user;
next();
}
catch (err) {
console.error(err);
return res.status(500).json({ status: false , msg: "Internal Server Error" });
}
} |
//models/Task.js const mongoose = require( "mongoose" );
const taskSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User" ,
required: true
},
description: {
type: String,
required: true ,
},
}, { timestamps: true
}); const Task = mongoose.model( "Task" , taskSchema);
module.exports = Task; |
//models.Users.js const mongoose = require( "mongoose" );
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [ true , "Please enter your name" ],
trim: true
},
email: {
type: String,
required: [ true , "Please enter your email" ],
trim: true ,
unique: true
},
password: {
type: String,
required: [ true , "Please enter your password" ],
},
joiningTime: {
type: Date,
default : Date.now
}
}, { timestamps: true
}); const User = mongoose.model( "User" , userSchema);
module.exports = User; |
//routes/authRoutes.js const express = require( "express" );
const router = express.Router(); const { signup, login } = require( "../controllers/authControllers" );
// Routes beginning with /api/auth router.post( "/signup" , signup);
router.post( "/login" , login);
module.exports = router; |
// routes/profileRoutes.js const express = require( "express" );
const router = express.Router(); const { getProfile } = require( "../controllers/profileControllers" );
const { verifyAccessToken } = require( "../middlewares.js" );
// Routes beginning with /api/profile router.get( "/" , verifyAccessToken, getProfile);
module.exports = router; |
// routes/taskRoutes.js const express = require( "express" );
const router = express.Router(); const { getTasks, getTask, postTask, putTask, deleteTask } = require( "../controllers/taskControllers" );
const { verifyAccessToken } = require( "../middlewares.js" );
// Routes beginning with /api/tasks router.get( "/" , verifyAccessToken, getTasks);
router.get( "/:taskId" , verifyAccessToken, getTask);
router.post( "/" , verifyAccessToken, postTask);
router.put( "/:taskId" , verifyAccessToken, putTask);
router. delete ( "/:taskId" , verifyAccessToken, deleteTask);
module.exports = router; |
//utils/token.js const jwt = require( "jsonwebtoken" );
const { ACCESS_TOKEN_SECRET } = process.env; const createAccessToken = (payload) => { return jwt.sign(payload, ACCESS_TOKEN_SECRET);
} module.exports = { createAccessToken,
} |
//utils/validation.js const mongoose = require( "mongoose" );
const validateEmail = (email) => { return String(email)
.toLowerCase()
.match(
/^(([^<>()[\]\\.,;:\s@ "]+(\.[^<>()[\]\\.,;:\s@" ]+)*)|
( ".+" ))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
);
}; const validateObjectId = (string) => { return mongoose.Types.ObjectId.isValid(string);
} module.exports = { validateEmail,
validateObjectId,
} |
Step 6: To start the server run the following code.
nodemon app.js
Step 7: Now go to the project’s root directory and create the frontend application
npx create-react-app frontend
cd frontend
Step 8: Install the required dependencies.
npm i axios react-redux react-router-dom react-toastify redux redux-thunk
Step 9: To use Tailwind CSS in the react application, first we need to install it
npm install tailwindcss@latest postcss@latest autoprefixer@latest
Then we will create a tailwind configuration file
npx tailwindcss init
Now setup the tailwind.config.js file
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {
colors: {
"primary": "#24ab8f",
"primary-dark": "#268d77",
},
animation: {
"loader": "loader 1s linear infinite",
},
keyframes: {
loader: {
"0%": { transform: "rotate(0) scale(1)" },
"50%": { transform: "rotate(180deg) scale(1.5)" },
"100%": { transform: "rotate(360deg) scale(1)" }
}
}
},
},
plugins: [],
}
Now include tailwind css in index.css file
@tailwind base;
@tailwind components;
@tailwind utilities;
Now you can use classes of Tailwind CSS in your files.
Folder Structure (Frontend):
Dependencies(Frontend): The updated dependencies in package.json file for frontend will look like.
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.6.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^9.1.0",
"react-router-dom": "^6.22.1",
"react-scripts": "5.0.1",
"react-toastify": "^10.0.4",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"web-vitals": "^2.1.4"
}
Step 10: Now add the following code in respective components in frontend part
/* index.css */ @tailwind base; @tailwind components; @tailwind utilities; body { font-family : "Roboto" , sans-serif ;
} |
//App.jsx import { useEffect } from "react" ;
import { useDispatch, useSelector } from "react-redux" ;
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom" ;
import Task from "./pages/Task" ;
import Home from "./pages/Home" ;
import Login from "./pages/Login" ;
import Signup from "./pages/Signup" ;
import { saveProfile } from "./redux/actions/authActions" ;
import NotFound from "./pages/NotFound" ;
function App() {
const authState = useSelector(state => state.authReducer);
const dispatch = useDispatch();
useEffect(() => {
const token = localStorage.getItem( "token" );
if (!token) return ;
dispatch(saveProfile(token));
}, [authState.isLoggedIn, dispatch]);
return (
<>
<BrowserRouter>
<Routes>
<Route path= "/" element={<Home />} />
<Route path= "/signup" element={authState.isLoggedIn ?
<Navigate to= "/" /> : <Signup />} />
<Route path= "/login" element={<Login />} />
<Route path= "/tasks/add" element={authState.isLoggedIn ?
<Task /> : <Navigate to= "/login"
state={{ redirectUrl: "/tasks/add" }} />} />
<Route path= "/tasks/:taskId"
element={authState.isLoggedIn ?
<Task /> : <Navigate to= "/login"
state={{ redirectUrl: window.location.pathname }} />} />
<Route path= "*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</>
);
} export default App;
|
//index.js import React from 'react' ;
import ReactDOM from 'react-dom/client' ;
import './index.css' ;
import App from './App' ;
import { Provider } from "react-redux"
import store from './redux/store' ;
import { ToastContainer } from 'react-toastify' ;
import 'react-toastify/dist/ReactToastify.css' ;
const root = ReactDOM.createRoot(document.getElementById( 'root' ));
root.render( <React.StrictMode>
<ToastContainer bodyStyle={{ fontFamily: "Roboto" }} />
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
); |
//validations/index.js const isValidEmail = (email) => { return String(email)
.toLowerCase()
.match(
/^(([^<>()[\]\\.,;:\s@ "]+(\.[^<>()[\]\\.,;:\s@" ]+)*)|
( ".+" ))@((\[[0 - 9]{ 1, 3}\.[0 - 9]{ 1, 3}\.[0 - 9]
{ 1, 3}\.[0 - 9]{ 1, 3}\])|
(([a - zA - Z\-0 - 9] +\.) +[a - zA - Z]{ 2,})) $ /
);
}; export const validate = (group, name, value) => { if (group === "signup" ) {
switch (name) {
case "name" : {
if (!value) return "This field is required" ;
return null ;
}
case "email" : {
if (!value) return "This field is required" ;
if (!isValidEmail(value))
return "Please enter valid email address" ;
return null ;
}
case "password" : {
if (!value) return "This field is required" ;
if (value.length < 4)
return "Password should be atleast 4 chars long" ;
return null ;
}
default : return null ;
}
}
else if (group === "login" ) {
switch (name) {
case "email" : {
if (!value) return "This field is required" ;
if (!isValidEmail(value))
return "Please enter valid email address" ;
return null ;
}
case "password" : {
if (!value) return "This field is required" ;
return null ;
}
default : return null ;
}
}
else if (group === "task" ) {
switch (name) {
case "description" : {
if (!value) return "This field is required" ;
if (value.length > 100) return "Max. limit is 100 characters." ;
return null ;
}
default : return null ;
}
}
else {
return null ;
}
} const validateManyFields = (group, list) => { const errors = [];
for (const field in list) {
const err = validate(group, field, list[field]);
if (err) errors.push({ field, err });
}
return errors;
} export default validateManyFields;
|
//redux/store.js import { applyMiddleware, createStore, compose } from "redux" ;
import thunk from "redux-thunk" ;
import rootReducer from "./reducers" ;
const middleware = [thunk]; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const store = createStore(rootReducer, composeEnhancers(applyMiddleware(...middleware))
); export default store;
|
//redux/reducers/authReducer.js import { LOGIN_FAILURE,
LOGIN_REQUEST,
LOGIN_SUCCESS,
LOGOUT,
SAVE_PROFILE
} from "../actions/actionTypes"
const initialState = { loading: false ,
user: {},
isLoggedIn: false ,
token: "" ,
successMsg: "" ,
errorMsg: "" ,
} const authReducer = (state = initialState, action) => { switch (action.type) {
case LOGIN_REQUEST:
return {
loading: true , user: {}, isLoggedIn: false ,
token: "" , successMsg: "" , errorMsg: "" ,
};
case LOGIN_SUCCESS:
return {
loading: false , user: action.payload.user,
isLoggedIn: true , token: action.payload.token,
successMsg: action.payload.msg, errorMsg: ""
};
case LOGIN_FAILURE:
return {
loading: false , user: {}, isLoggedIn: false ,
token: "" , successMsg: "" , errorMsg: action.payload.msg
};
case LOGOUT:
return {
loading: false , user: {}, isLoggedIn: false , token: "" ,
successMsg: "" , errorMsg: ""
}
case SAVE_PROFILE:
return {
loading: false , user: action.payload.user,
isLoggedIn: true , token: action.payload.token,
successMsg: "" , errorMsg: ""
}
default :
return state;
}
} export default authReducer;
|
//redux/reducers/index.js import { combineReducers } from "redux"
import authReducer from "./authReducer"
const rootReducer = combineReducers({ authReducer,
}); export default rootReducer;
|
//redux/actions/actionTypes.js export const LOGIN_REQUEST = 'LOGIN_REQUEST'
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
export const LOGIN_FAILURE = 'LOGIN_FAILURE'
export const LOGOUT = 'LOGOUT'
export const SAVE_PROFILE = 'SAVE_PROFILE'
export const SIGNUP_REQUEST = 'SIGNUP_REQUEST'
export const SIGNUP_SUCCESS = 'SIGNUP_SUCCESS'
export const SIGNUP_FAILURE = 'SIGNUP_FAILURE'
|
//redux/actions/authActions.js import api from "../../api"
import { LOGIN_FAILURE,
LOGIN_REQUEST,
LOGIN_SUCCESS,
LOGOUT
, SAVE_PROFILE
} from "./actionTypes"
import { toast } from "react-toastify" ;
export const postLoginData = (email, password) => async (dispatch) => {
try {
dispatch({ type: LOGIN_REQUEST });
const { data } = await api.post
( '/auth/login' , { email, password });
dispatch({
type: LOGIN_SUCCESS,
payload: data,
});
localStorage.setItem( 'token' , data.token);
toast.success(data.msg);
}
catch (error) {
const msg = error.response?.data?.msg || error.message;
dispatch({
type: LOGIN_FAILURE,
payload: { msg }
})
toast.error(msg);
}
}
export const saveProfile = (token) => async (dispatch) => {
try {
const { data } = await api.get( '/profile' , {
headers: { Authorization: token }
});
dispatch({
type: SAVE_PROFILE,
payload: { user: data.user, token },
});
}
catch (error) {
// console.log(error);
}
}
export const logout = () => (dispatch) => { localStorage.removeItem( 'token' );
dispatch({ type: LOGOUT });
document.location.href = '/' ;
} |
//api/index.jsx import axios from "axios" ;
const api = axios.create({ baseURL: "/api" ,
}); export default api;
|
//components/utils/Input.jsx import React from "react" ;
const Input = ({ id,
name,
type,
value,
className = "" ,
disabled = false ,
placeholder,
onChange,
}) => { return (
<input
id={id}
type={type}
name={name}
value={value}
disabled={disabled}
className={`block w-full mt-2
px-3 py-2 text-gray-600 rounded-[4px]
border-2 border-gray-100 ${disabled ? "bg-gray-50" : ""
} focus:border-primary transition
outline-none hover:border-gray-300 ${className}`}
placeholder={placeholder}
onChange={onChange}
/>
);
}; export default Input;
export const Textarea = ({ id,
name,
type,
value,
className = "" ,
placeholder,
onChange,
}) => { return (
<textarea
id={id}
type={type}
name={name}
value={value}
className={`block w-full h-40
mt-2 px-3 py-2 text-gray-600
rounded-[4px] border-2 border-gray-100
focus:border-primary transition outline-none
hover:border-gray-300 ${className}`}
placeholder={placeholder}
onChange={onChange}
/>
);
}; |
// components/utils/Loader.jsx import React from 'react'
const Loader = () => { return (
<>
<div className= 'w-8 h-8 my-8 mx-auto' >
<div className= "w-full h-full rounded-full
border-[3px] border-indigo-600
border-b-transparent animate-loader" ></div>
</div>
</>
)
} export default Loader
|
//utils/Tooltip.jsx import React, { useRef, useState } from "react" ;
import ReactDom from "react-dom" ;
const Portal = ({ children }) => { return ReactDom.createPortal(children, document.body);
}; const Tooltip = ({ children, text,
position = "bottom" , space = 5 }) => {
if (!React.isValidElement(children)) {
children = children[0];
}
const [open, setOpen] = useState( false );
const tooltipRef = useRef();
const elementRef = useRef();
const handleMouseEnter = () => {
setOpen( true );
const { x, y } = getPoint(
elementRef.current,
tooltipRef.current,
position,
space
);
tooltipRef.current.style.left = `${x}px`;
tooltipRef.current.style.top = `${y}px`;
};
const getPoint = (element, tooltip, position, space) => {
const eleRect = element.getBoundingClientRect();
const pt = { x: 0, y: 0 };
switch (position) {
case "bottom" : {
pt.x = eleRect.left +
(element.offsetWidth - tooltip.offsetWidth) / 2;
pt.y = eleRect.bottom + (space + 10);
break ;
}
case "left" : {
pt.x = eleRect.left -
(tooltip.offsetWidth + (space + 10));
pt.y = eleRect.top +
(element.offsetHeight - tooltip.offsetHeight) / 2;
break ;
}
case "right" : {
pt.x = eleRect.right + (space + 10);
pt.y = eleRect.top +
(element.offsetHeight - tooltip.offsetHeight) / 2;
break ;
}
case "top" : {
pt.x = eleRect.left +
(element.offsetWidth - tooltip.offsetWidth) / 2;
pt.y = eleRect.top -
(tooltip.offsetHeight + (space + 10));
break ;
}
default : {
break ;
}
}
return pt;
};
const tooltipClasses = `fixed transition ${open ? "opacity-100" : "opacity-0 "
} pointer-events-none z-50 rounded-md
bg-black text-white px-4 py-2 text-center
w-max max-w-[150px]
${position === "top" &&
" after:absolute after:content-['']
after: left - 1 / 2 after: top - full after: -translate - x - 1 / 2
after: border - [10px] after: border - transparent after: border - t - black"
} ${
position === "bottom" &&
" after:absolute after:content-[''] after:left-1/2
after: bottom - full after: -translate - x - 1 / 2
after: border - [10px] after: border - transparent
after: border - b - black"
} ${
position === "left" &&
" after:absolute after:content-[''] after:top-1/2
after: left - full after: -translate - y - 1 / 2 after: border - [10px]
after: border - transparent after: border - l - black"
} ${
position === "right" &&
" after:absolute after:content-[''] after:top-1/2
after: right - full after: -translate - y - 1 / 2 after: border - [10px]
after: border - transparent after: border - r - black"
} `; return (
<>
{React.cloneElement(children, {
onMouseEnter: handleMouseEnter,
onMouseLeave: () => setOpen( false ),
ref: elementRef,
})}
<Portal>
<div ref={tooltipRef} className={tooltipClasses}>
{text}
</div>
</Portal>
</>
);
}; export default Tooltip;
|
//components/LoginForm.jsx import React, { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom' ;
import validateManyFields from '../validations' ;
import Input from './utils/Input' ;
import { useDispatch, useSelector } from "react-redux" ;
import { postLoginData } from '../redux/actions/authActions' ;
import Loader from './utils/Loader' ;
import { useEffect } from 'react' ;
const LoginForm = ({ redirectUrl }) => { const [formErrors, setFormErrors] = useState({});
const [formData, setFormData] = useState({
email: "" ,
password: ""
});
const navigate = useNavigate();
const authState = useSelector(state => state.authReducer);
const { loading, isLoggedIn } = authState;
const dispatch = useDispatch();
useEffect(() => {
if (isLoggedIn) {
navigate(redirectUrl || "/" );
}
}, [authState, redirectUrl, isLoggedIn, navigate]);
const handleChange = e => {
setFormData({
...formData, [e.target.name]: e.target.value
});
}
const handleSubmit = e => {
e.preventDefault();
const errors = validateManyFields( "login" , formData);
setFormErrors({});
if (errors.length > 0) {
setFormErrors(errors.reduce((total, ob) =>
({ ...total, [ob.field]: ob.err }), {}));
return ;
}
dispatch(postLoginData(formData.email, formData.password));
}
const fieldError = (field) => (
<p className={`mt-1 text-pink-600 text-sm
${formErrors[field] ? "block" : "hidden" }`}>
<i className= 'mr-2 fa-solid fa-circle-exclamation' ></i>
{formErrors[field]}
</p>
)
return (
<>
<form className= 'm-auto my-16 max-w-[500px] bg-white
p-8 border-2 shadow-md rounded-md' >
{loading ? (
<Loader />
) : (
<>
<h2 className= 'text-center mb-4' >Welcome user, please login here</h2>
<div className= "mb-4" >
<label htmlFor= "email" className= "after:content-['*']
after:ml-0.5 after:text-red-500" >Email</label>
<Input type= "text" name= "email" id= "email"
value={formData.email} placeholder= "youremail@domain.com"
onChange={handleChange} />
{fieldError( "email" )}
</div>
<div className= "mb-4" >
<label htmlFor= "password" className= "after:content-['*']
after:ml-0.5 after:text-red-500" >Password</label>
<Input type= "password" name= "password" id= "password"
value={formData.password} placeholder= "Your password.."
onChange={handleChange} />
{fieldError( "password" )}
</div>
<button className= 'bg-primary text-white px-4 py-2
font-medium hover:bg-primary-dark'
onClick={handleSubmit}>Submit</button>
<div className= 'pt-4' >
<Link to= "/signup" className= 'text-blue-400' >
Don't have an account? Signup here</Link>
</div>
</>
)}
</form>
</>
)
} export default LoginForm
|
//components/Navbr.jsx import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux' ;
import { Link } from 'react-router-dom' ;
import { logout } from '../redux/actions/authActions' ;
const Navbar = () => { const authState = useSelector(state => state.authReducer);
const dispatch = useDispatch();
const [isNavbarOpen, setIsNavbarOpen] = useState( false );
const toggleNavbar = () => {
setIsNavbarOpen(!isNavbarOpen);
}
const handleLogoutClick = () => {
dispatch(logout());
}
return (
<>
<header className= 'flex justify-between sticky
top-0 p-4 bg-white shadow-sm items-center' >
<h2 className= 'cursor-pointer uppercase font-medium' >
<Link to= "/" > Task Manager </Link>
</h2>
<ul className= 'hidden md:flex gap-4 uppercase font-medium' >
{authState.isLoggedIn ? (
<>
<li className= "bg-blue-500 text-white
hover:bg-blue-600 font-medium rounded-md" >
<Link to= '/tasks/add' className= 'block w-full
h-full px-4 py-2' > <i className= "fa-solid fa-plus" ></i>
Add task </Link>
</li>
<li className= 'py-2 px-3 cursor-pointer hover:bg-gray-200
transition rounded-sm' onClick={handleLogoutClick}>Logout</li>
</>
) : (
<li className= 'py-2 px-3 cursor-pointer text-primary
hover:bg-gray-100 transition rounded-sm' ><Link to= "/login" >
Login</Link></li>
)}
</ul>
<span className= 'md:hidden cursor-pointer'
onClick={toggleNavbar}><i className= "fa-solid fa-bars" >
</i></span>
<div className={`absolute md:hidden right-0 top-0 bottom-0
transition ${(isNavbarOpen === true ) ? 'translate-x-0' : 'translate-x-full' }
bg-gray-100 shadow-md w-screen sm:w-9/12 h-screen`}>
<div className= 'flex' >
<span className= 'm-4 ml-auto cursor-pointer'
onClick={toggleNavbar}><i className= "fa-solid fa-xmark" ></i></span>
</div>
<ul className= 'flex flex-col gap-4 uppercase font-medium text-center' >
{authState.isLoggedIn ? (
<>
<li className= "bg-blue-500 text-white
hover:bg-blue-600 font-medium transition py-2 px-3" >
<Link to= '/tasks/add' className= 'block w-full h-full' >
<i className= "fa-solid fa-plus" ></i> Add task </Link>
</li>
<li className= 'py-2 px-3 cursor-pointer hover:bg-gray-200
transition rounded-sm' onClick={handleLogoutClick}>Logout</li>
</>
) : (
<li className= 'py-2 px-3 cursor-pointer text-primary
hover:bg-gray-200 transition rounded-sm' >
<Link to= "/login" >Login</Link></li>
)}
</ul>
</div>
</header>
</>
)
} export default Navbar
|
//components/SignupForm.jsx import React, { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom' ;
import useFetch from '../hooks/useFetch' ;
import validateManyFields from '../validations' ;
import Input from './utils/Input' ;
import Loader from './utils/Loader' ;
const SignupForm = () => { const [formErrors, setFormErrors] = useState({});
const [formData, setFormData] = useState({
name: "" ,
email: "" ,
password: ""
});
const [fetchData, { loading }] = useFetch();
const navigate = useNavigate();
const handleChange = e => {
setFormData({
...formData, [e.target.name]: e.target.value
});
}
const handleSubmit = e => {
e.preventDefault();
const errors = validateManyFields( "signup" , formData);
setFormErrors({});
if (errors.length > 0) {
setFormErrors(errors.reduce((total, ob) =>
({ ...total, [ob.field]: ob.err }), {}));
return ;
}
const config = { url: "/auth/signup" , method: "post" , data: formData };
fetchData(config).then(() => {
navigate( "/login" );
});
}
const fieldError = (field) => (
<p className={`mt-1 text-pink-600 text-sm
${formErrors[field] ? "block" : "hidden" }`}>
<i className= 'mr-2 fa-solid fa-circle-exclamation' ></i>
{formErrors[field]}
</p>
)
return (
<>
<form className= 'm-auto my-16 max-w-[500px]
p-8 bg-white border-2 shadow-md rounded-md' >
{loading ? (
<Loader />
) : (
<>
<h2 className= 'text-center mb-4' >
Welcome user, please signup here</h2>
<div className= "mb-4" >
<label htmlFor= "name" className= "after:content-['*']
after:ml-0.5 after:text-red-500" >Name</label>
<Input type= "text" name= "name" id= "name"
value={formData.name} placeholder= "Your name"
onChange={handleChange} />
{fieldError( "name" )}
</div>
<div className= "mb-4" >
<label htmlFor= "email" className= "after:content-['*']
after:ml-0.5 after:text-red-500" >Email</label>
<Input type= "text" name= "email" id= "email"
value={formData.email} placeholder= "youremail@domain.com"
onChange={handleChange} />
{fieldError( "email" )}
</div>
<div className= "mb-4" >
<label htmlFor= "password" className= "after:content-['*']
after:ml-0.5 after:text-red-500" >Password</label>
<Input type= "password" name= "password" id= "password"
value={formData.password} placeholder= "Your password.."
onChange={handleChange} />
{fieldError( "password" )}
</div>
<button className= 'bg-primary text-white px-4 py-2
font-medium hover:bg-primary-dark'
onClick={handleSubmit}>Submit</button>
<div className= 'pt-4' >
<Link to= "/login" className= 'text-blue-400' >
Already have an account? Login here</Link>
</div>
</>
)}
</form>
</>
)
} export default SignupForm
|
//components/Task.jsx import React, { useCallback, useEffect, useState } from 'react'
import { useSelector } from 'react-redux' ;
import { Link } from 'react-router-dom' ;
import useFetch from '../hooks/useFetch' ;
import Loader from './utils/Loader' ;
import Tooltip from './utils/Tooltip' ;
const Tasks = () => { const authState = useSelector(state =>
state.authReducer);
const [tasks, setTasks] = useState([]);
const [fetchData, { loading }] = useFetch();
const fetchTasks = useCallback(() => {
const config = {
url: "/tasks" , method: "get" ,
headers: { Authorization: authState.token }
};
fetchData(config, { showSuccessToast: false })
.then(data => setTasks(data.tasks));
}, [authState.token, fetchData]);
useEffect(() => {
if (!authState.isLoggedIn) return ;
fetchTasks();
}, [authState.isLoggedIn, fetchTasks]);
const handleDelete = (id) => {
const config = {
url: `/tasks/${id}`,
method: "delete" , headers: { Authorization: authState.token }
};
fetchData(config)
.then(() => fetchTasks());
}
return (
<>
<div className= "my-2 mx-auto max-w-[700px] py-4" >
{tasks.length !== 0 && <h2
className= 'my-2 ml-2 md:ml-0 text-xl' >
Your tasks ({tasks.length})</h2>}
{loading ? (
<Loader />
) : (
<div>
{tasks.length === 0 ? (
<div className= 'w-[600px] h-[300px]
flex items-center justify-center gap-4' >
<span>No tasks found</span>
<Link to= "/tasks/add" className= "bg-blue-500
text-white hover:bg-blue-600 font-medium
rounded-md px-4 py-2" >+ Add new task </Link>
</div>
) : (
tasks.map((task, index) => (
<div key={task._id} className= 'bg-white my-4 p-4
text-gray-600 rounded-md shadow-md' >
<div className= 'flex' >
<span className= 'font-medium' >
Task #{index + 1}</span>
<Tooltip text={ "Edit this task" } position={ "top" }>
<Link to={`/tasks/${task._id}`} className= 'ml-auto
mr-2 text-green-600 cursor-pointer' >
<i className= "fa-solid fa-pen" ></i>
</Link>
</Tooltip>
<Tooltip text={ "Delete this task" } position={ "top" }>
<span className= 'text-red-500 cursor-pointer'
onClick={() => handleDelete(task._id)}>
<i className= "fa-solid fa-trash" ></i>
</span>
</Tooltip>
</div>
<div className= 'whitespace-pre' >{task.description}</div>
</div>
))
)}
</div>
)}
</div>
</>
)
} export default Tasks
|
//hooks/useFetch.jsx import { useCallback, useState } from "react"
import { toast } from "react-toastify" ;
import api from "../api" ;
const useFetch = () => { const [state, setState] = useState({
loading: false ,
data: null ,
successMsg: "" ,
errorMsg: "" ,
});
const fetchData = useCallback
(async (config, otherOptions) => {
const { showSuccessToast = true ,
showErrorToast = true } = otherOptions || {};
setState(state => ({ ...state, loading: true }));
try {
const { data } = await api.request(config);
setState({
loading: false ,
data,
successMsg: data.msg || "success" ,
errorMsg: ""
});
if (showSuccessToast) toast.success(data.msg);
return Promise.resolve(data);
}
catch (error) {
const msg = error.response?.data?.msg || error.message || "error" ;
setState({
loading: false ,
data: null ,
errorMsg: msg,
successMsg: ""
});
if (showErrorToast) toast.error(msg);
return Promise.reject();
}
}, []);
return [fetchData, state];
} export default useFetch
|
//layouts/MainLayout.jsx import React from 'react'
import Navbar from '../components/Navbar' ;
const MainLayout = ({ children }) => { return (
<>
<div className= 'relative bg-gray-50
h-screen w-screen overflow-x-hidden' >
<Navbar />
{children}
</div>
</>
)
} export default MainLayout;
|
//pages/Home.jsx import React, { useEffect } from 'react'
import { useSelector } from 'react-redux' ;
import { Link } from 'react-router-dom' ;
import Tasks from '../components/Tasks' ;
import MainLayout from '../layouts/MainLayout' ;
const Home = () => { const authState = useSelector(state =>
state.authReducer);
const { isLoggedIn } = authState;
useEffect(() => {
document.title = authState.isLoggedIn ?
`${authState.user.name} 's tasks` : "Task Manager";
}, [authState]);
return (
<>
<MainLayout>
{!isLoggedIn ? (
<div className=' bg-primary text-white
h-[40vh] py-8 text-center '>
<h1 className=' text-2xl '>
Welcome to Task Manager App</h1>
<Link to="/signup" className=' mt-10
text-xl block space-x-2 hover:space-x-4 '>
<span className=' transition-[margin] '>
Join now to manage your tasks</span>
<span className=' relative ml-4 text-base
transition-[margin] '>
<i className="fa-solid fa-arrow-right"></i></span>
</Link>
</div>
) : (
<>
<h1 className=' text-lg mt-8 mx-8 border-b
border-b-gray-300'>Welcome {authState.user.name}</h1>
<Tasks />
</>
)}
</MainLayout>
</>
)
} export default Home
|
//pages/Login.jsx import React, { useEffect } from 'react'
import { useLocation } from 'react-router-dom' ;
import LoginForm from '../components/LoginForm' ;
import MainLayout from '../layouts/MainLayout'
const Login = () => { const { state } = useLocation();
const redirectUrl = state?.redirectUrl || null ;
useEffect(() => {
document.title = "Login" ;
}, []);
return (
<>
<MainLayout>
<LoginForm redirectUrl={redirectUrl} />
</MainLayout>
</>
)
} export default Login
|
//pages/NotFound.jsx import React from 'react'
import MainLayout from '../layouts/MainLayout'
const NotFound = () => { return (
<MainLayout>
<div className= 'w-full py-16 text-center' >
<h1 className= 'text-7xl my-8' >404</h1>
<h2 className= 'text-xl' >
The page you are looking for doesn't exist</h2>
</div>
</MainLayout>
)
} export default NotFound
|
//pages/Signup.jsx import React, { useEffect } from 'react'
import SignupForm from '../components/SignupForm' ;
import MainLayout from '../layouts/MainLayout'
const Signup = () => { useEffect(() => {
document.title = "Signup" ;
}, []);
return (
<>
<MainLayout>
<SignupForm />
</MainLayout>
</>
)
} export default Signup
|
//pages/Task.jsx import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux' ;
import { useNavigate, useParams } from 'react-router-dom' ;
import { Textarea } from '../components/utils/Input' ;
import Loader from '../components/utils/Loader' ;
import useFetch from '../hooks/useFetch' ;
import MainLayout from '../layouts/MainLayout' ;
import validateManyFields from '../validations' ;
const Task = () => { const authState = useSelector(state => state.authReducer);
const navigate = useNavigate();
const [fetchData, { loading }] = useFetch();
const { taskId } = useParams();
const mode = taskId === undefined ? "add" : "update" ;
const [task, setTask] = useState( null );
const [formData, setFormData] = useState({
description: ""
});
const [formErrors, setFormErrors] = useState({});
useEffect(() => {
document.title = mode === "add" ? "Add task" : "Update Task" ;
}, [mode]);
useEffect(() => {
if (mode === "update" ) {
const config = {
url: `/tasks/${taskId}`, method: "get" ,
headers: { Authorization: authState.token }
};
fetchData(config, { showSuccessToast: false })
.then((data) => {
setTask(data.task);
setFormData({ description: data.task.description });
});
}
}, [mode, authState, taskId, fetchData]);
const handleChange = e => {
setFormData({
...formData, [e.target.name]: e.target.value
});
}
const handleReset = e => {
e.preventDefault();
setFormData({
description: task.description
});
}
const handleSubmit = e => {
e.preventDefault();
const errors = validateManyFields( "task" , formData);
setFormErrors({});
if (errors.length > 0) {
setFormErrors(errors.reduce((total, ob) =>
({ ...total, [ob.field]: ob.err }), {}));
return ;
}
if (mode === "add" ) {
const config = {
url: "/tasks" , method: "post" ,
data: formData, headers: { Authorization: authState.token }
};
fetchData(config).then(() => {
navigate( "/" );
});
}
else {
const config = {
url: `/tasks/${taskId}`, method: "put" ,
data: formData, headers: { Authorization: authState.token }
};
fetchData(config).then(() => {
navigate( "/" );
});
}
}
const fieldError = (field) => (
<p className={`mt-1 text-pink-600 text-sm
${formErrors[field] ? "block" : "hidden" }`}>
<i className= 'mr-2 fa-solid fa-circle-exclamation' ></i>
{formErrors[field]}
</p>
)
return (
<>
<MainLayout>
<form className= 'm-auto my-16 max-w-[1000px]
bg-white p-8 border-2 shadow-md rounded-md' >
{loading ? (
<Loader />
) : (
<>
<h2 className= 'text-center mb-4' >{mode === "add" ?
"Add New Task" : "Edit Task" }</h2>
<div className= "mb-4" >
<label htmlFor= "description" >Description</label>
<Textarea type= "description" name= "description"
id= "description" value={formData.description} placeholder= "Write here.."
onChange={handleChange} />
{fieldError( "description" )}
</div>
<button className= 'bg-primary text-white px-4 py-2 font-medium hover:bg-primary-dark'
onClick={handleSubmit}>{mode === "add" ? "Add task" : "Update Task" }</button>
<button className= 'ml-4 bg-red-500 text-white px-4 py-2 font-medium'
onClick={() => navigate( "/" )}>Cancel</button>
{mode === "update" && <button className= 'ml-4 bg-blue-500 text-white px-4
py-2 font-medium hover:bg-blue-600'
onClick={handleReset}>Reset</button>}
</>
)}
</form>
</MainLayout>
</>
)
} export default Task
|
Step 11: Now start the react application
npm start