├── .babelrc
├── .env
├── .eslintrc.json
├── .gitignore
├── .sequelizerc
├── README.md
├── api
├── app.js
├── config
│ └── config.js
├── controllers
│ ├── authController.js
│ ├── todoItemsController.js
│ └── todosController.js
├── middlewares
│ ├── auth.js
│ └── authorize.js
├── migrations
│ ├── 20191207170237-create-user.js
│ ├── 20200126150323-create-todo.js
│ └── 20200126150424-create-todo-item.js
├── models
│ ├── index.js
│ ├── todo.js
│ ├── todoitem.js
│ └── user.js
├── routes
│ └── index.js
└── utils
│ ├── index.js
│ └── sendEmail.js
├── build
└── app.js
├── client
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── index.html
│ └── todo.jpg
├── src
│ ├── actions
│ │ ├── auth.js
│ │ ├── index.js
│ │ └── todos.js
│ ├── apiCall
│ │ └── index.js
│ ├── components
│ │ ├── App.js
│ │ ├── auth
│ │ │ ├── SignIn.jsx
│ │ │ ├── SignUp.jsx
│ │ │ └── Styles.js
│ │ ├── landing
│ │ │ ├── Landing.jsx
│ │ │ └── Styles.js
│ │ ├── layouts
│ │ │ ├── Footer.js
│ │ │ ├── Layout.jsx
│ │ │ ├── NavBar.jsx
│ │ │ └── Styles.js
│ │ └── todos
│ │ │ ├── CreateTask.jsx
│ │ │ ├── CreateTodoModal.jsx
│ │ │ ├── ListTasks.jsx
│ │ │ ├── ListTodos.jsx
│ │ │ ├── RenderTodos.jsx
│ │ │ └── Styles.js
│ ├── context
│ │ ├── authContext.js
│ │ ├── createDataContext.jsx
│ │ └── todosContext.js
│ ├── index.js
│ └── reducers
│ │ ├── auth.js
│ │ └── todos.js
└── yarn.lock
├── package-lock.json
└── package.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"]
3 | }
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | DB_DEV_URL="postgres://postgres:awa@localhost:5432/todo-dev"
2 | DB_TESt_URL="postgres://postgres:awa@localhost:5432/todo-test"
3 | DB_PROD_URL=
4 | JWT_SECRET="ourjwtsecret"
5 | SENDGRID_API_KEY="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"
6 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "env": {
4 | "node": true,
5 | "es6": true,
6 | "mocha": true
7 | },
8 | "extends": "airbnb-base",
9 | "settings": {
10 | "import/resolver": {
11 | "node": {
12 | "extensions": [
13 | ".js",
14 | ".jsx"
15 | ]
16 | }
17 | }
18 | },
19 | "parser": "babel-eslint",
20 | "parserOptions": {
21 | "ecmaVersion": 6,
22 | "sourceType": "module",
23 | "ecmaFeatures": {
24 | "modules": true,
25 | "jsx": true
26 | }
27 | },
28 | "plugins": ["import", "react"],
29 | "rules": {
30 | "comma-dangle": 0
31 | }
32 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.sequelizerc:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | 'config': path.resolve('./api/config', 'config.js'),
5 | 'models-path': path.resolve('./api', 'models'),
6 | 'seeders-path': path.resolve('./api', 'seeders'),
7 | 'migrations-path': path.resolve('./api', 'migrations'),
8 | };
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Best To Do List Application
2 | A ReactJS Hooks/Context, Styled-components, Bootstrap, Node, Express, Sequelize and Postgres To Do List application companion repo to YouTube tutorial here: [https://www.youtube.com/watch?v=gK-M8u08f8Y&list=PL9g7odpg28fTgk7O1zXixYt0uzqBs9jCm](https://www.youtube.com/watch?v=gK-M8u08f8Y&list=PL9g7odpg28fTgk7O1zXixYt0uzqBs9jCm)
3 |
4 | ## How to test the application.
5 |
6 | - Clone the repo
7 | - In the root directory of the application, run `npm install`.
8 | - Start the api server by running the command `npm run dev`.
9 | - After running that, open another terminal tab and run `cd client` to change into the client directory.
10 | - While in the client, run `npm install` again to install dependencies for the client side.
11 | - And lastly, `npm start` to completely start the application.
12 |
--------------------------------------------------------------------------------
/api/app.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import bodyParser from 'body-parser';
3 | import cors from 'cors';
4 | import { routes } from './routes';
5 |
6 | const app = express();
7 |
8 | app.use(cors({ credentials: true, origin: true }));
9 |
10 | app.use(bodyParser.urlencoded({ extended: false }));
11 | app.use(bodyParser.json());
12 |
13 | routes(app);
14 |
15 | const port = process.env.PORT || 3002;
16 | app.listen(port, () => console.log(`Server ready at http://localhost:${port}`));
17 |
--------------------------------------------------------------------------------
/api/config/config.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | module.exports = {
4 | development: {
5 | database: 'todo-dev',
6 | use_env_variables: 'DB_DEV_URL',
7 | dialect: 'postgres',
8 | },
9 | test: {
10 | database: 'todo-test',
11 | use_env_variables: 'DB_TEST_URL',
12 | dialect: 'postgres',
13 | },
14 | production: {
15 | database: 'todo-prod',
16 | use_env_variables: 'DB_PROD_URL',
17 | dialect: 'postgres',
18 | },
19 | }
--------------------------------------------------------------------------------
/api/controllers/authController.js:
--------------------------------------------------------------------------------
1 | import validator from 'validator';
2 | import sendEmail from '../utils/sendEmail';
3 | import models from '../models';
4 | import { hashPassword, jwtToken, comparePassword } from '../utils';
5 |
6 | const { User } = models;
7 |
8 | const auth = {
9 | async signUp(req, res, next) {
10 | try {
11 | const { name, email, password } = req.body;
12 | const hash = hashPassword(password);
13 | const user = await User.create({ name, email, password: hash });
14 | const token = jwtToken.createToken(user);
15 | const { id } = user;
16 | return res.status(201).send({ token, user: { id, name, email } });
17 | } catch (e) {
18 | return next(new Error(e));
19 | }
20 | },
21 |
22 | async signIn(req, res, next) {
23 | try {
24 | const { email, password } = req.body;
25 | const user = await User.findOne({ where: { email } });
26 | if (user && comparePassword(password, user.password)) {
27 | const { name, id } = user;
28 | const token = jwtToken.createToken(user);
29 | return res.status(200).send({ token, user: { id, name, email } });
30 | }
31 | return res.status(400).send({ error: 'invalid email/password combination ' });
32 | } catch (e) {
33 | return next(new Error(e));
34 | }
35 | },
36 |
37 | async sendResetLink(req, res, next) {
38 | try {
39 | const { email } = req.body;
40 | const user = await User.findOne({ where: { email } });
41 | if (!email) {
42 | return res.status(400).send({ error: 'Email is required' });
43 | }
44 | if (!validator.isEmail(email)) {
45 | return res.status(400).send({ error: 'Invalid email' });
46 | }
47 | if (!user) {
48 | return res.status(404).send({ error: 'User not found' });
49 | }
50 | const token = jwtToken.createToken(user);
51 | const link = `${req.protocol}://localhost:5000/reset_password/${token}`;
52 | await sendEmail(
53 | email,
54 | 'noreply@todo.com',
55 | 'Best To Do password reset',
56 | `
57 |
Click the link below to reset your password
58 | ${link}
59 | `
60 | );
61 | return res.status(200).send({ message: 'Password reset link has been successfully sent to your inbox' });
62 | } catch (e) {
63 | return next(new Error(e));
64 | }
65 | },
66 |
67 | async resetPassword(req, res, next) {
68 | try {
69 | const { password } = req.body;
70 | const { token } = req.params;
71 | const decoded = jwtToken.verifyToken(token);
72 | const hash = hashPassword(password);
73 | const updatedUser = await User.update(
74 | { password: hash },
75 | {
76 | where: { id: decoded.userId },
77 | returning: true,
78 | plain: true,
79 | }
80 | );
81 | const { id, name, email } = updatedUser[1];
82 | return res.status(200).send({ token, user: { id, name, email } });
83 | } catch (e) {
84 | return next(new Error(e));
85 | }
86 | }
87 | };
88 |
89 | export default auth;
90 |
--------------------------------------------------------------------------------
/api/controllers/todoItemsController.js:
--------------------------------------------------------------------------------
1 | import models from '../models';
2 |
3 | const { TodoItem, Todo } = models;
4 |
5 | const todoItems = {
6 | async create(req, res, next) {
7 | try {
8 | const { text, todoId } = req.body;
9 | // Validation
10 | if (!text) { return res.status(400).send({ error: 'Text is required' }); }
11 | if (!todoId) { return res.status(400).send({ error: 'todoId is required' }); }
12 | const item = await TodoItem.create({ text, todoId });
13 | return res.status(201).send(item);
14 | } catch (e) {
15 | return next(new Error(e));
16 | }
17 | },
18 |
19 | async fetchAll(req, res, next) {
20 | try {
21 | const { todoId } = req.params;
22 | // Validation
23 | if (!todoId) { return res.status(400).send({ error: 'todoId is required' }); }
24 | const items = await TodoItem.findAll({
25 | where: { todoId },
26 | include: [{
27 | model: Todo,
28 | as: 'todo'
29 | }],
30 | });
31 | return res.status(200).send(items);
32 | } catch (e) {
33 | return next(new Error(e));
34 | }
35 | },
36 |
37 | async fetchOne(req, res, next) {
38 | try {
39 | const { todoItemId } = req.params;
40 | // Validation
41 | if (!todoItemId) { return res.status(400).send({ error: 'todoItemId is required' }); }
42 | const items = await TodoItem.findOne({
43 | where: { id: todoItemId },
44 | include: [{
45 | model: Todo,
46 | as: 'todo'
47 | }],
48 | });
49 | return res.status(200).send(items);
50 | } catch (e) {
51 | return next(new Error(e));
52 | }
53 | },
54 |
55 | async update(req, res, next) {
56 | try {
57 | const { text, isCompleted } = req.body;
58 | const { todoItemId } = req.params;
59 | // Validation
60 | if (!todoItemId) { return res.status(400).send({ error: 'todoItemId is required' }); }
61 | const item = await TodoItem.findOne({
62 | where: { id: todoItemId },
63 | });
64 | if (!item) {
65 | return res.status(404).send({ error: 'Item does not exist' });
66 | }
67 | const updatedItem = await TodoItem.update(
68 | { text: text || item.text, isCompleted },
69 | {
70 | where: { id: req.params.todoItemId },
71 | returning: true,
72 | plain: true,
73 | }
74 | );
75 | return res.status(200).send(updatedItem[1]);
76 | } catch (e) {
77 | return next(new Error(e));
78 | }
79 | },
80 |
81 | async delete(req, res, next) {
82 | try {
83 | const { todoItemId } = req.params;
84 | // Validation
85 | if (!todoItemId) { return res.status(400).send({ error: 'todoItemId is required' }); }
86 | const item = await TodoItem.findOne({
87 | where: { id: todoItemId },
88 | });
89 | if (!item) {
90 | return res.status(404).send({ error: 'Item does not exist' });
91 | }
92 | await item.destroy();
93 | return res.status(200).send({});
94 | } catch (e) {
95 | return next(new Error(e));
96 | }
97 | }
98 | };
99 |
100 | export default todoItems;
101 |
--------------------------------------------------------------------------------
/api/controllers/todosController.js:
--------------------------------------------------------------------------------
1 | import models from '../models';
2 |
3 | const { Todo, TodoItem } = models;
4 |
5 | const todos = {
6 | async create({ body, decoded }, res, next) {
7 | try {
8 | const { title } = body;
9 | const { userId } = decoded;
10 | const todo = await Todo.create({ title, userId });
11 | return res.status(201).send(todo);
12 | } catch (e) {
13 | return next(new Error(e));
14 | }
15 | },
16 |
17 | async fetchAll({ decoded }, res, next) {
18 | try {
19 | const myTodos = await Todo.findAll({
20 | where: { userId: decoded.userId },
21 | include: [{
22 | model: TodoItem,
23 | as: 'todoItems'
24 | }],
25 | });
26 | return res.status(200).send(myTodos);
27 | } catch (e) {
28 | return next(new Error(e));
29 | }
30 | },
31 |
32 | async fetchOne({ params, decoded }, res, next) {
33 | try {
34 | const myTodo = await Todo.findOne({
35 | where: { id: params.todoId, userId: decoded.userId },
36 | include: [{
37 | model: TodoItem,
38 | as: 'todoItems'
39 | }],
40 | });
41 | if (!myTodo) {
42 | return res.status(404).send({ error: 'Todo not found' });
43 | }
44 | return res.status(200).send(myTodo);
45 | } catch (e) {
46 | return next(new Error(e));
47 | }
48 | },
49 |
50 | async update({ body, decoded, params }, res, next) {
51 | try {
52 | const todo = await Todo.findOne({ where: { id: params.todoId, userId: decoded.userId } });
53 | if (!todo) {
54 | return res.status(400).send({ error: 'Wrong todo id' });
55 | }
56 | const updatedTodo = await Todo.update({ title: body.title || todo.title },
57 | {
58 | where: { id: todo.id },
59 | returning: true,
60 | plain: true
61 | },);
62 | return res.status(200).send(updatedTodo[1]);
63 | } catch (e) {
64 | return next(new Error(e));
65 | }
66 | },
67 |
68 | async delete({ params, decoded }, res, next) {
69 | try {
70 | const todo = await Todo.findOne({ where: { id: params.todoId, userId: decoded.userId } });
71 | if (!todo) {
72 | return res.status(400).send({ error: 'Wrong todo id' });
73 | }
74 | await todo.destroy();
75 | return res.status(200).send({});
76 | } catch (e) {
77 | return next(new Error(e));
78 | }
79 | }
80 | };
81 |
82 | export default todos;
83 |
--------------------------------------------------------------------------------
/api/middlewares/auth.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable consistent-return */
2 | import validator from 'validator';
3 | import { User } from '../models';
4 |
5 | export default async (req, res, next) => {
6 | const { name, email, password } = req.body;
7 | if (!validator.isEmail) {
8 | return res.status(400).send({ error: 'invalid email address' });
9 | }
10 | if (!email) {
11 | return res.status(400).send({ error: 'email is required' });
12 | }
13 | if (!name) {
14 | return res.status(400).send({ error: 'name is required' });
15 | }
16 | if (!password) {
17 | return res.status(400).send({ error: 'password is required' });
18 | }
19 | const user = await User.findOne({ where: { email } });
20 | if (user) {
21 | return res.status(409).send({ error: 'user exist already' });
22 | }
23 | next();
24 | };
25 |
--------------------------------------------------------------------------------
/api/middlewares/authorize.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 | import { config } from 'dotenv';
3 | import models from '../models';
4 |
5 | const { User } = models;
6 |
7 | config();
8 |
9 | export default (req, res, next) => {
10 | if (!req.headers.authorization) {
11 | return res.status(401).send({ error: 'Unauthorized' });
12 | }
13 |
14 | const token = req.headers.authorization.split(' ')[1];
15 | return jwt.verify(token, process.env.JWT_SECRET, { expiresIn: '24h' }, (error, decoded) => {
16 | if (error) {
17 | return res.status(401).send({ error });
18 | }
19 | req.decoded = decoded;
20 | return User.findByPk(decoded.userId)
21 | .then((user) => {
22 | if (!user) {
23 | return res.status(401).send({ error: 'User does not exist' });
24 | }
25 | return next();
26 | });
27 | });
28 | };
29 |
--------------------------------------------------------------------------------
/api/migrations/20191207170237-create-user.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = {
3 | up: (queryInterface, Sequelize) => queryInterface.createTable('Users', {
4 | id: {
5 | allowNull: false,
6 | autoIncrement: true,
7 | primaryKey: true,
8 | type: Sequelize.INTEGER
9 | },
10 | name: {
11 | type: Sequelize.STRING
12 | },
13 | email: {
14 | type: Sequelize.STRING,
15 | unique: true,
16 | },
17 | password: {
18 | type: Sequelize.STRING
19 | },
20 | createdAt: {
21 | allowNull: false,
22 | type: Sequelize.DATE
23 | },
24 | updatedAt: {
25 | allowNull: false,
26 | type: Sequelize.DATE
27 | }
28 | }),
29 | down: (queryInterface) => queryInterface.dropTable('Users')
30 | };
31 |
--------------------------------------------------------------------------------
/api/migrations/20200126150323-create-todo.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = {
3 | up: (queryInterface, Sequelize) => queryInterface.createTable('Todos', {
4 | id: {
5 | allowNull: false,
6 | autoIncrement: true,
7 | primaryKey: true,
8 | type: Sequelize.INTEGER
9 | },
10 | title: {
11 | type: Sequelize.STRING
12 | },
13 | userId: {
14 | type: Sequelize.INTEGER,
15 | allowNull: false,
16 | references: {
17 | model: 'Users',
18 | key: 'id'
19 | }
20 | },
21 | createdAt: {
22 | allowNull: false,
23 | type: Sequelize.DATE
24 | },
25 | updatedAt: {
26 | allowNull: false,
27 | type: Sequelize.DATE
28 | }
29 | }),
30 | down: (queryInterface) => queryInterface.dropTable('Todos')
31 | };
32 |
--------------------------------------------------------------------------------
/api/migrations/20200126150424-create-todo-item.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = {
3 | up: (queryInterface, Sequelize) => queryInterface.createTable('TodoItems', {
4 | id: {
5 | allowNull: false,
6 | autoIncrement: true,
7 | primaryKey: true,
8 | type: Sequelize.INTEGER
9 | },
10 | text: {
11 | type: Sequelize.STRING,
12 | allowNull: false,
13 | },
14 | todoId: {
15 | type: Sequelize.INTEGER,
16 | allowNull: false,
17 | references: {
18 | model: 'Todos',
19 | key: 'id'
20 | }
21 | },
22 | isCompleted: {
23 | type: Sequelize.BOOLEAN,
24 | defaultValue: false,
25 | },
26 | createdAt: {
27 | allowNull: false,
28 | type: Sequelize.DATE
29 | },
30 | updatedAt: {
31 | allowNull: false,
32 | type: Sequelize.DATE
33 | }
34 | }),
35 | down: (queryInterface) => queryInterface.dropTable('TodoItems')
36 | };
37 |
--------------------------------------------------------------------------------
/api/models/index.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import Sequelize from 'sequelize';
4 | import envVars from '../config/config';
5 |
6 | const basename = path.basename(__filename);
7 | const env = process.env.NODE_ENV || 'development';
8 | const config = envVars[env];
9 | const db = {};
10 |
11 | let sequelize;
12 | if (config.use_env_variable) {
13 | sequelize = new Sequelize(process.env[config.use_env_variable], config);
14 | } else {
15 | sequelize = new Sequelize(config.database, config.username, config.password, config);
16 | }
17 |
18 | fs
19 | .readdirSync(__dirname)
20 | .filter((file) => (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'))
21 | .forEach((file) => {
22 | const model = sequelize.import(path.join(__dirname, file));
23 | db[model.name] = model;
24 | });
25 |
26 | Object.keys(db).forEach((modelName) => {
27 | if (db[modelName].associate) {
28 | db[modelName].associate(db);
29 | }
30 | });
31 |
32 | db.sequelize = sequelize;
33 | db.Sequelize = Sequelize;
34 |
35 | module.exports = db;
36 |
--------------------------------------------------------------------------------
/api/models/todo.js:
--------------------------------------------------------------------------------
1 | module.exports = (sequelize, DataTypes) => {
2 | const Todo = sequelize.define('Todo', {
3 | title: DataTypes.STRING,
4 | userId: DataTypes.INTEGER
5 | }, {});
6 | Todo.associate = (models) => {
7 | // associations can be defined here
8 | Todo.belongsTo(models.User, {
9 | as: 'user',
10 | foreignKey: 'userId'
11 | });
12 | Todo.hasMany(models.TodoItem, {
13 | as: 'todoItems',
14 | foreignKey: 'todoId'
15 | });
16 | };
17 | return Todo;
18 | };
19 |
--------------------------------------------------------------------------------
/api/models/todoitem.js:
--------------------------------------------------------------------------------
1 | module.exports = (sequelize, DataTypes) => {
2 | const TodoItem = sequelize.define('TodoItem', {
3 | text: DataTypes.STRING,
4 | todoId: DataTypes.INTEGER,
5 | isCompleted: DataTypes.BOOLEAN
6 | }, {});
7 | TodoItem.associate = (models) => {
8 | // associations can be defined here
9 | TodoItem.belongsTo(models.Todo, {
10 | as: 'todo',
11 | foreignKey: 'todoId'
12 | });
13 | };
14 | return TodoItem;
15 | };
16 |
--------------------------------------------------------------------------------
/api/models/user.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = (sequelize, DataTypes) => {
3 | const User = sequelize.define('User', {
4 | name: DataTypes.STRING,
5 | email: DataTypes.STRING,
6 | password: DataTypes.STRING
7 | }, {});
8 | User.associate = (models) => {
9 | // associations can be defined here
10 | User.hasMany(models.Todo, {
11 | as: 'todos',
12 | foreignKey: 'userId',
13 | });
14 | };
15 | return User;
16 | };
17 |
--------------------------------------------------------------------------------
/api/routes/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/prefer-default-export */
2 | import auth from '../controllers/authController';
3 | import validateAuth from '../middlewares/auth';
4 | import todos from '../controllers/todosController';
5 | import authorize from '../middlewares/authorize';
6 | import todoItems from '../controllers/todoItemsController';
7 |
8 | export const routes = (app) => {
9 | app.get('/', (req, res) => res.send({ message: 'Welcome to Todo API' }));
10 |
11 | app.post('/api/auth/sign_up', validateAuth, auth.signUp);
12 | app.post('/api/auth/sign_in', auth.signIn);
13 | app.post('/api/auth/forgot_password', auth.sendResetLink);
14 | app.post('/reset_password/:token', auth.resetPassword);
15 |
16 | app.post('/api/todos', authorize, todos.create);
17 | app.get('/api/todos', authorize, todos.fetchAll);
18 | app.get('/api/todos/:todoId', authorize, todos.fetchOne);
19 | app.put('/api/todos/:todoId', authorize, todos.update);
20 | app.delete('/api/todos/:todoId', authorize, todos.delete);
21 |
22 | app.post('/api/todoItems', todoItems.create);
23 | app.get('/api/todos/:todoId/todoItems', todoItems.fetchAll);
24 | app.get('/api/todoItems/:todoItemId', todoItems.fetchOne);
25 | app.put('/api/todoItems/:todoItemId', todoItems.update);
26 | app.delete('/api/todoItems/:todoItemId', todoItems.delete);
27 | };
28 |
--------------------------------------------------------------------------------
/api/utils/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/prefer-default-export */
2 | import jwt from 'jsonwebtoken';
3 | import bcrypt from 'bcryptjs';
4 | import { config } from 'dotenv';
5 |
6 | config();
7 |
8 | export const jwtToken = {
9 | createToken({ id, email }) {
10 | return jwt.sign(
11 | { userId: id, email },
12 | process.env.JWT_SECRET,
13 | { expiresIn: '24h' }
14 | );
15 | },
16 | verifyToken(token) {
17 | const decoded = jwt.verify(token, process.env.JWT_SECRET, { expiresIn: '24h' });
18 | return decoded;
19 | }
20 | };
21 |
22 | export const hashPassword = (password) => bcrypt.hashSync(password, 10);
23 | export const comparePassword = (password, hash) => bcrypt.compareSync(password, hash);
24 |
--------------------------------------------------------------------------------
/api/utils/sendEmail.js:
--------------------------------------------------------------------------------
1 | import { config } from 'dotenv';
2 | import sgMail from '@sendgrid/mail';
3 |
4 | config();
5 |
6 | sgMail.setApiKey(process.env.SENDGRID_API_KEY);
7 |
8 | const sendEmail = (receiver, source, subject, content) => {
9 | try {
10 | const data = {
11 | to: receiver,
12 | from: source,
13 | subject,
14 | html: content,
15 | };
16 | return sgMail.send(data);
17 | } catch (e) {
18 | return new Error(e);
19 | }
20 | };
21 |
22 | export default sendEmail;
23 |
--------------------------------------------------------------------------------
/build/app.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var _express = _interopRequireDefault(require("express"));
3 |
4 | var _bodyParser = _interopRequireDefault(require("body-parser"));
5 |
6 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
7 |
8 | var app = (0, _express["default"])();
9 | app.use(_bodyParser["default"].urlencoded({
10 | extended: false
11 | }));
12 | app.use(_bodyParser["default"].json());
13 | var port = process.env.app.PORT || 5000;
14 | app.listen(port, function () {
15 | return console.log("Server ready at http://localhost:".concat(port));
16 | });
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `yarn start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `yarn test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `yarn build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `yarn eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `yarn build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "axios": "^0.19.2",
10 | "react": "^16.12.0",
11 | "react-dom": "^16.12.0",
12 | "react-scripts": "3.3.1",
13 | "styled-components": "^5.0.1"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
20 | },
21 | "eslintConfig": {
22 | "extends": "react-app"
23 | },
24 | "browserslist": {
25 | "production": [
26 | ">0.2%",
27 | "not dead",
28 | "not op_mini all"
29 | ],
30 | "development": [
31 | "last 1 chrome version",
32 | "last 1 firefox version",
33 | "last 1 safari version"
34 | ]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
20 | React App
21 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/client/public/todo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dieudonneAwa/react-nodejs-todo-list/fd7cf1f300be1c0476ad7c5b83d8e88c228d8f1b/client/public/todo.jpg
--------------------------------------------------------------------------------
/client/src/actions/auth.js:
--------------------------------------------------------------------------------
1 | import apiCall from '../apiCall';
2 |
3 | export const signUp = (dispatch) => async (user) => {
4 | try {
5 | dispatch({ type: 'SIGNUP_USER_LOADING' });
6 | const res = await apiCall('/auth/sign_up', 'post', user);
7 | dispatch({ type: 'SIGNUP_USER_SUCCESS', payload: res.data });
8 | return res;
9 | } catch (err) {
10 | return dispatch({ type: 'SIGNUP_USER_FAILURE', payload: err.response.data });
11 | }
12 | };
13 |
14 | export const setCurrentUser = (dispatch) => (Cookies, jwtDecode) => {
15 | try {
16 | dispatch({ type: 'SET_CURRENT_USER_LOADING' });
17 | const payload = jwtDecode(Cookies.get('token'));
18 | dispatch({ type: 'SET_CURRENT_USER_SUCCESS', payload });
19 | } catch (err) {
20 | dispatch({ type: 'SET_CURRENT_USER_FAILURE', payload: err })
21 | }
22 | }
23 |
24 | export const signIn = (dispatch) => async (user) => {
25 | try {
26 | dispatch({ type: 'SIGNIN_USER_LOADING' });
27 | const res = await apiCall('/auth/sign_in', 'post', user);
28 | dispatch({ type: 'SIGNIN_USER_SUCCESS', payload: res.data });
29 | return res;
30 | } catch (err) {
31 | return dispatch({ type: 'SIGNIN_USER_FAILURE', payload: err.response.data })
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/client/src/actions/index.js:
--------------------------------------------------------------------------------
1 | export * from './auth';
2 | export * from './todos';
3 |
--------------------------------------------------------------------------------
/client/src/actions/todos.js:
--------------------------------------------------------------------------------
1 | import apiCall from "../apiCall";
2 |
3 | export const createTodo = (dispatch) => async (todo, Cookies) => {
4 | try {
5 | dispatch({ type: 'CREATE_TODO_LOADING' });
6 | const res = await apiCall('/todos', 'post', todo, Cookies.get('token'));
7 | dispatch({ type: 'CREATE_TODO_SUCCESS', payload: res.data })
8 | return res;
9 | } catch (err) {
10 | return dispatch({ type: 'CREATE_TODO_FAILURE' })
11 | }
12 | };
13 |
14 | export const fetchTodos = (dispatch) => async (Cookies) => {
15 | try {
16 | dispatch({ type: 'FETCH_TODOS_LOADING' });
17 | const res = await apiCall('/todos', 'get', null, Cookies.get('token'));
18 | dispatch({ type: 'FETCH_TODOS_SUCCESS', payload: res.data })
19 | return res;
20 | } catch (err) {
21 | if (err && err.response) {
22 | return dispatch({ type: 'FETCH_TODOS_FAILURE', payload: err.response.data })
23 | }
24 | }
25 | }
26 |
27 | export const deleteTodo = (dispatch) => async (todoId, Cookies) => {
28 | try {
29 | dispatch({ type: 'DELETE_TODO_LOADING' });
30 | const res = await apiCall(`/todos/${todoId}`, 'delete', null, Cookies.get('token'));
31 | dispatch({ type: 'DELETE_TODO_SUCCESS', payload: todoId });
32 | return res;
33 | } catch (err) {
34 | console.log(err)
35 | return dispatch({ type: 'DELETE_TODO_FAILURE', payload: err.response.data })
36 | }
37 | }
38 |
39 | export const createTask = (dispatch) => async (task, Cookies) => {
40 | try {
41 | dispatch({ type: 'CREATE_TASK_LOADING' });
42 | const res = await apiCall('/todoItems', 'post', task, Cookies.get('token'));
43 | dispatch({ type: 'CREATE_TASK_SUCCESS', payload: res.data });
44 | return res;
45 | } catch (err) {
46 | return dispatch({ type: 'CREATE_TASK_FAILURE' })
47 | }
48 | }
49 |
50 | export const markTaskAsDone = (dispatch) => async (task) => {
51 | try {
52 | dispatch({ type: 'MARK_AS_DONE_LOADING' });
53 | const res = await apiCall(`/todoItems/${task.id}`, 'put', task);
54 | dispatch({ type: 'MARK_AS_DONE_SUCCESS', payload: res.data });
55 | return res;
56 | } catch (err) {
57 | return dispatch({ type: 'MARK_AS_DONE_FAILURE', payload: err.response.data });
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/client/src/apiCall/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const composeToken = (token) => token ? { Authorization: `Bearer ${token}` } : {};
4 |
5 | const apiCall = (url, method, body = {}, token = '') => axios({
6 | method,
7 | url: `http://localhost:3002/api${url}`,
8 | data: body,
9 | headers: {
10 | ...composeToken(token)
11 | }
12 | });
13 |
14 | export default apiCall;
15 |
--------------------------------------------------------------------------------
/client/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { Route, BrowserRouter as Router } from 'react-router-dom';
3 | import Cookies from 'js-cookie';
4 | import Landing from './landing/Landing';
5 | import { Provider, Context } from '../context/authContext';
6 | import SignUp from './auth/SignUp';
7 | import SignIn from './auth/SignIn';
8 | import ListTodos from './todos/ListTodos';
9 |
10 | function App() {
11 | const { signUp, signIn } = useContext(Context);
12 |
13 | return (
14 |
15 |
16 |
17 | ( {
18 | const res = await signUp(user);
19 | if (res) {
20 | Cookies.set('token', res.data.token);
21 | history.push('/todos');
22 | }
23 | }} />)
24 | } />
25 | ( {
26 | const res = await signIn(user);
27 | if (res) {
28 | Cookies.set('token', res.data.token);
29 | history.push('/todos');
30 | }
31 | }} />)} />
32 | ()} />
33 |
34 |
35 | );
36 | }
37 |
38 | export default () => {
39 | return (
40 |
41 |
42 |
43 | )
44 | };
45 |
--------------------------------------------------------------------------------
/client/src/components/auth/SignIn.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from 'react';
2 | import classnames from 'classnames';
3 | import { NavLink } from 'react-router-dom';
4 | import Layout from '../layouts/Layout';
5 | import { AuthFormWrapper } from './Styles';
6 | import { Context } from '../../context/authContext';
7 |
8 | const SignIn = ({ signIn }) => {
9 | const { state } = useContext(Context);
10 | const [email, setemail] = useState('');
11 | const [password, setpassword] = useState('');
12 | const [emailErr, setemailErr] = useState('');
13 | const [passwordErr, setpasswordErr] = useState('');
14 |
15 | const handleChange = (e, name) => {
16 | const user = {};
17 | const emailRegEx = RegExp(
18 | /^(([^<>()\[\]\\.,;:\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,}))$/
19 | );
20 | user[name] = e.target.value;
21 | // validations
22 | switch (name) {
23 | case 'email':
24 | setemail(user.email);
25 | !emailRegEx.test(user.email) ? setemailErr('Invalid Email!') : setemailErr('');
26 | break;
27 | case 'password':
28 | setpassword(user.password);
29 | user.password.length < 8 ? setpasswordErr('Password must be at least 8 characters!') : setpasswordErr('');
30 | break;
31 | default:
32 | break;
33 | }
34 | }
35 |
36 | const handleSignIn = async (e) => {
37 | e.preventDefault();
38 | if (email && password && !emailErr && !passwordErr) {
39 | await signIn({ email, password });
40 | }
41 | }
42 |
43 | return (
44 |
45 |
46 | Sign In
47 | {state.signInErr &&
48 | {state.signInErr}
49 |
}
50 |
83 |
84 | Don't yet have an account? Sign Up
85 |
86 |
87 |
88 | )
89 | }
90 |
91 | export default SignIn
92 |
--------------------------------------------------------------------------------
/client/src/components/auth/SignUp.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from 'react';
2 | import classnames from 'classnames';
3 | import { NavLink } from 'react-router-dom';
4 | import Layout from '../layouts/Layout';
5 | import { AuthFormWrapper } from './Styles';
6 | import { Context } from '../../context/authContext';
7 |
8 | const SignUp = ({ signUp }) => {
9 | const { state } = useContext(Context);
10 | const [name, setname] = useState('');
11 | const [email, setemail] = useState('');
12 | const [password, setpassword] = useState('');
13 | const [confPassword, setconfPassword] = useState('');
14 | const [nameErr, setnameErr] = useState('');
15 | const [emailErr, setemailErr] = useState('');
16 | const [passwordErr, setpasswordErr] = useState('');
17 | const [confPasswordErr, setconfPasswordErr] = useState('');
18 |
19 | const handleChange = (e, name) => {
20 | const user = {};
21 | const emailRegEx = RegExp(
22 | /^(([^<>()\[\]\\.,;:\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,}))$/
23 | );
24 | user[name] = e.target.value;
25 | // validations
26 | switch (name) {
27 | case 'name':
28 | setname(user.name);
29 | user.name.length < 3 ? setnameErr('Name must be at least 3 characters!') : setnameErr('');
30 | break;
31 | case 'email':
32 | setemail(user.email);
33 | !emailRegEx.test(user.email) ? setemailErr('Invalid Email!') : setemailErr('');
34 | break;
35 | case 'password':
36 | setpassword(user.password);
37 | user.password.length < 8 ? setpasswordErr('Password must be at least 8 characters!') : setpasswordErr('');
38 | break;
39 | case 'confPassword':
40 | setconfPassword(user.confPassword);
41 | user.confPassword !== password ? setconfPasswordErr('Passwords do not match!') : setconfPasswordErr('');
42 | break;
43 | default:
44 | break;
45 | }
46 | }
47 |
48 | const handleSignUp = async (e) => {
49 | e.preventDefault();
50 | if (name && email && password && confPassword && !nameErr && !emailErr && !passwordErr && !confPasswordErr) {
51 | await signUp({ name, email, password });
52 | }
53 | }
54 |
55 | return (
56 |
57 |
58 | Create an Account
59 | {state.signUpErr &&
60 | {state.signUpErr}
61 |
}
62 |
125 |
126 | Already have an account? Sign In
127 |
128 |
129 |
130 | )
131 | }
132 |
133 | export default SignUp
134 |
--------------------------------------------------------------------------------
/client/src/components/auth/Styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const AuthFormWrapper = styled.div`
4 | width: 40%;
5 | border-radius: 5px;
6 | margin: 70px auto;
7 | `;
8 |
--------------------------------------------------------------------------------
/client/src/components/landing/Landing.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Main } from './Styles'
3 | import NavBar from '../layouts/NavBar';
4 |
5 | const Landing = () => {
6 | return (
7 | <>
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | >
19 | )
20 | }
21 |
22 | export default Landing;
23 |
--------------------------------------------------------------------------------
/client/src/components/landing/Styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Main = styled.div`
4 | background-image: url('/todo.jpg');
5 | filter: saturate(200%);
6 | background-image:
7 | linear-gradient(rgba(1, 1, 1, 0.52), rgba(8,8,8, 0.73)),
8 | url('/todo.jpg');
9 | width: 100%;
10 | height: 100vh;
11 | background-size: cover;
12 |
13 | .container-fluid {
14 | height: 100%;
15 | }
16 | .row {
17 | margin-top: 300px;
18 |
19 | .btn {
20 | border: 1px solid #06A82A;
21 | background: #FFFFFF;
22 | font-size: x-large;
23 | border-radius: 5px;
24 | color: #06A82A;
25 | font-weight: 600;
26 | }
27 | }
28 | `;
29 |
--------------------------------------------------------------------------------
/client/src/components/layouts/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const Wrapper = styled.footer`
5 | width: 100%;
6 | height: 70px;
7 | border-top: 1px solid #ccc;
8 | background-color: #fff;
9 | position: absolute;
10 | bottom: 0;
11 | `;
12 |
13 | const Footer = () => {
14 | return (
15 |
16 | © 2020 FullStack Valley.
17 |
18 | );
19 | }
20 |
21 | export default Footer
22 |
--------------------------------------------------------------------------------
/client/src/components/layouts/Layout.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import NavBar from './NavBar';
4 | import Footer from './Footer';
5 |
6 | const Wrapper = styled.div`
7 | width: 100%;
8 | min-height: 100vh;
9 | position: relative;
10 | `;
11 |
12 | const Layout = ({ children }) => {
13 | return (
14 |
15 |
16 | {children}
17 |
18 |
19 | )
20 | }
21 |
22 | export default Layout
23 |
--------------------------------------------------------------------------------
/client/src/components/layouts/NavBar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect } from 'react';
2 | import Cookies from 'js-cookie';
3 | import jwtDecode from 'jwt-decode';
4 | import { NavLink, useHistory } from 'react-router-dom';
5 | import { Nav } from './Styles';
6 | import { Context as AuthContext } from '../../context/authContext';
7 |
8 | const NavBar = () => {
9 | const { state: { user }, setCurrentUser } = useContext(AuthContext);
10 | const history = useHistory();
11 |
12 | useEffect(() => {
13 | if (Cookies.get('token')) {
14 | setCurrentUser(Cookies, jwtDecode);
15 | }
16 | }, []);
17 |
18 | return (
19 |
42 | )
43 | };
44 |
45 | const mapStateToProps = ({ auth }) => {
46 | console.log(auth);
47 | return { ...auth }
48 | }
49 |
50 | export default NavBar;
51 |
--------------------------------------------------------------------------------
/client/src/components/layouts/Styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Nav = styled.nav`
4 | display: inline-block;
5 | width: 100%;
6 | height: 60px;
7 | color: #fff;
8 | background-color: #06A82A;
9 | .logo {
10 | font-family: Lora;
11 | font-style: italic;
12 | font-weight: bold;
13 | line-height: 55px;
14 | font-size: xx-large;
15 | a {
16 | text-decoration: none;
17 | color: #ffffff;
18 | }
19 | }
20 | .auth-btns {
21 | width: 100%;
22 | .btn {
23 | float: right;
24 | color: #fff;
25 | font-weight: 600;
26 | }
27 | .sign-in {
28 | line-height: 45px;
29 | }
30 | .sign-up {
31 | height: 30px;
32 | width: 80px;
33 | background: rgba(255, 215, 3, 0.25);
34 | border: 1px solid #07709D;
35 | border-radius: 5px;
36 | padding: 3px;
37 | margin-top: 15px;
38 | }
39 | }
40 | `;
41 |
--------------------------------------------------------------------------------
/client/src/components/todos/CreateTask.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const CreateTask = ({ handleSubmit, handleChange }) => {
4 | return (
5 |
11 | )
12 | }
13 |
14 | export default CreateTask
15 |
--------------------------------------------------------------------------------
/client/src/components/todos/CreateTodoModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useContext } from 'react';
2 | import Cookies from 'js-cookie';
3 | import { ModalWrapper } from './Styles';
4 | import { Context as TodosContext, Provider as TodosProvider } from '../../context/todosContext';
5 |
6 | const CreateTodoModal = (props) => {
7 | const { createTodo } = useContext(TodosContext);
8 | const [todo, settodo] = useState('');
9 | let myRef;
10 | useEffect(() => {
11 | document.addEventListener('click', closeTodoModal);
12 | return () => {
13 | document.removeEventListener('click', closeTodoModal);
14 | }
15 | }, []);
16 |
17 | const closeTodoModal = (e) => {
18 | if (myRef && !myRef.contains(e.target)) {
19 | props.closeModal();
20 | }
21 | }
22 |
23 | const handleChange = (e) => {
24 | e.preventDefault();
25 | settodo(e.target.value);
26 | }
27 |
28 | const handleSubmit = async (e) => {
29 | e.preventDefault();
30 | console.log(todo)
31 | if (todo) {
32 | await createTodo({ title: todo }, Cookies);
33 | props.closeModal();
34 | }
35 | }
36 |
37 | return (
38 |
39 |
63 |
64 | );
65 | };
66 |
67 | export default CreateTodoModal;
68 |
--------------------------------------------------------------------------------
/client/src/components/todos/ListTasks.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Tick } from './Styles';
3 |
4 | const ListTasks = ({ handleDone, todo }) => {
5 | if (todo?.todoItems) {
6 | return (
7 | <>
8 | {todo.todoItems.map((task) => (
9 |
10 | {task.text}
11 | {task.isCompleted ?
12 |
13 |
: (
14 |
15 |
16 |
17 | )}
18 |
19 | ))}
20 | >
21 | )
22 | }
23 | return null;
24 | }
25 |
26 | export default ListTasks;
27 |
--------------------------------------------------------------------------------
/client/src/components/todos/ListTodos.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useContext } from 'react';
2 | import Cookies from 'js-cookie';
3 | import Layout from '../layouts/Layout';
4 | import { TodoList } from './Styles';
5 | import CreateTodoModal from './CreateTodoModal';
6 | import { Context as TodosContext, Provider as TodosProvider } from '../../context/todosContext';
7 | import RenderTodos from './RenderTodos';
8 |
9 |
10 | const ListTodos = () => {
11 | const { state, fetchTodos } = useContext(TodosContext);
12 | const [showModal, setshowModal] = useState(false);
13 | const [todoId, setTodoId] = useState(null)
14 |
15 | useEffect(() => {
16 | (async () => {
17 | await fetchTodos(Cookies);
18 | })();
19 | }, []);
20 |
21 | const handleShowModal = () => {
22 | setshowModal(true);
23 | }
24 |
25 | const handleCloseModal = () => {
26 | setshowModal(false);
27 | }
28 |
29 | const handleAddTask = (todo) => {
30 | // setshowAddTaskInput(true);
31 | setTodoId(todo.id);
32 | }
33 |
34 | return (
35 |
36 |
37 | {showModal && }
38 |
39 |
40 |
41 |
My Todos
42 |
43 |
44 |
49 |
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | export default () => {
60 | return (
61 |
62 |
63 |
64 | )
65 | };
66 |
--------------------------------------------------------------------------------
/client/src/components/todos/RenderTodos.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useContext } from 'react';
2 | import Cookies from 'js-cookie';
3 | import { Context as TodosContext } from '../../context/todosContext';
4 | import CreateTask from './CreateTask';
5 | import ListTasks from './ListTasks';
6 |
7 | const RenderTodos = () => {
8 | const [showCreateTask, setshowCreateTask] = useState(false);
9 | const [todoId, settodoId] = useState(0);
10 | const [task, settask] = useState('')
11 | const { state, createTask, markTaskAsDone, fetchTodos, deleteTodo } = useContext(TodosContext);
12 |
13 | const handleAddTask = (todo) => {
14 | setshowCreateTask(true);
15 | settodoId(todo.id);
16 | }
17 |
18 | const handleChange = (e) => {
19 | settask(e.target.value);
20 | }
21 |
22 | const handleSubmit = async (e) => {
23 | e.preventDefault();
24 | if (task) {
25 | const res = await createTask({ text: task, todoId }, Cookies);
26 | if (res) {
27 | await fetchTodos(Cookies);
28 | }
29 | }
30 | }
31 |
32 | const handleDeleteTodo = async (todo) => {
33 | const res = await deleteTodo(todo.id, Cookies);
34 | if (res) {
35 | await fetchTodos(Cookies);
36 | }
37 | }
38 |
39 | const handleDone = async (task) => {
40 | const res = await markTaskAsDone({
41 | id: task.id,
42 | isCompleted: true,
43 | text: task.text
44 | });
45 | if (res) {
46 | setshowCreateTask(false);
47 | await fetchTodos(Cookies);
48 | }
49 | }
50 |
51 | return (
52 |
53 |
54 | {state.todos && state.todos.sort((a, b) => (a.id - b.id)).map((todo, i) => {
55 | i++;
56 | return (
57 |
58 |
61 |
64 |
{i}. {todo.title}
65 |
68 | {showCreateTask && todoId === todo.id &&
}
69 |
70 | )
71 | })}
72 |
73 |
74 | )
75 | }
76 |
77 | export default RenderTodos
78 |
--------------------------------------------------------------------------------
/client/src/components/todos/Styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { CheckDouble } from '@styled-icons/boxicons-regular/CheckDouble';
3 |
4 | export const Tick = styled(CheckDouble)`
5 | width: 30px;
6 | color: #06A82A;
7 | margin-right: -10px;
8 | `;
9 |
10 | export const TodoList = styled.div`
11 | width: 100%;
12 | min-height: 90vh;
13 | margin-bottom: 100px;
14 |
15 | .header {
16 | color: #076822;
17 | font-weight: 400;
18 | }
19 | .todos {
20 | background: rgba(204, 204, 204, 0.1);
21 | border: 1px solid rgba(10, 248, 77, 0.5);
22 | padding: 10px;
23 | }
24 | .todo {
25 | border: 1px solid #ccc;
26 | margin: 10px;
27 | position: relative;
28 | padding: 0 20px 10px 20px;
29 | background-color: #fff;
30 | min-height: 100px;
31 |
32 | .float-right button {
33 | background-color: #fff;
34 | color: #06A82A;
35 | font-weight: 600;
36 | margin-right: -10px;
37 | border: none;
38 | }
39 | .float-right button:hover {
40 | font-weight: 600;
41 | color: #000;
42 | }
43 |
44 | h5 {
45 | font-weight: 600;
46 | padding: 10px 0;
47 | }
48 |
49 | .delete-btn {
50 | position: absolute;
51 | right: 0;
52 | width: 25px;
53 | background-color: white;
54 | border: 1px solid red;
55 | }
56 | .add-btn {
57 | position: absolute;
58 | right: 28px;
59 | background-color: #06A82A;
60 | border: 1px solid #06A82A;
61 | color: #fff;
62 | width: 100px;
63 | }
64 |
65 | .create-task {
66 | border: 1px solid #ccc;
67 | background-color: #fff;
68 | margin-top: 1px;
69 | border-radius: 3px;
70 | padding: 1px;
71 |
72 | input {
73 | width: 95%;
74 | border: none;
75 | padding: 5px;
76 | outline: none;
77 | }
78 | button {
79 | float: right;
80 | padding: 5px;
81 | height: 100%;
82 | border: none;
83 | border-left: 1px solid #ccc;
84 | }
85 | }
86 | }
87 | `;
88 |
89 |
90 | export const ModalWrapper = styled.div`
91 | width: 100vw;
92 | height: 100vh;
93 | background-color: rgba(0, 0, 0, .4);
94 | display: inline-flex;
95 | justify-content: center;
96 | align-items: center;
97 | z-index: 20;
98 | position: fixed;
99 | top: 0;
100 | right: 0;
101 | left: 0;
102 | form {
103 | margin-top: -100px;
104 | background: #FFFFFF;
105 | border: 1px solid rgba(10, 248, 77, 0.5);
106 | border-radius: 3px;
107 | width: 500px;
108 | padding: 30px;
109 | z-index: 20;
110 | box-shadow: 0 0 10px rgba(10, 248, 77, 0.2)
111 | }
112 | `;
113 |
--------------------------------------------------------------------------------
/client/src/context/authContext.js:
--------------------------------------------------------------------------------
1 | import createDataContext from './createDataContext';
2 | import authReducer from '../reducers/auth';
3 | import { signUp, signIn, setCurrentUser } from '../actions';
4 |
5 | export const initialState = {
6 | user: null,
7 | isAuthenticated: false,
8 | signUpErr: '',
9 | signInErr: ''
10 | };
11 |
12 | export const { Context, Provider } = createDataContext(
13 | authReducer,
14 | { signUp, signIn, setCurrentUser },
15 | initialState,
16 | )
17 |
18 |
--------------------------------------------------------------------------------
/client/src/context/createDataContext.jsx:
--------------------------------------------------------------------------------
1 | import React, { useReducer, createContext } from 'react';
2 |
3 | export default (reducer, actions, initialState) => {
4 | const Context = createContext();
5 |
6 | const Provider = ({ children }) => {
7 | const [state, dispatch] = useReducer(reducer, initialState);
8 |
9 | // actions = { signUp: (dispatch) => (user) => {}}
10 | const boundActions = {};
11 | for (let key in actions) {
12 | boundActions[key] = actions[key](dispatch);
13 | }
14 |
15 | return (
16 |
17 | {children}
18 |
19 | );
20 | }
21 | return { Context, Provider };
22 | };
23 |
--------------------------------------------------------------------------------
/client/src/context/todosContext.js:
--------------------------------------------------------------------------------
1 | import createDataContext from './createDataContext';
2 | import todosReducer from '../reducers/todos';
3 | import { createTodo, fetchTodos, createTask, markTaskAsDone, deleteTodo } from '../actions/todos';
4 |
5 | export const initialState = {
6 | createTodoError: '',
7 | fetchTodosError: '',
8 | todos: []
9 | };
10 |
11 | export const { Context, Provider } = createDataContext(
12 | todosReducer,
13 | { createTodo, fetchTodos, createTask, markTaskAsDone, deleteTodo },
14 | initialState
15 | );
16 |
17 |
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './components/App';
4 |
5 | ReactDOM.render(
6 | ,
7 | document.getElementById('root')
8 | );
9 |
--------------------------------------------------------------------------------
/client/src/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import { initialState } from "../context/authContext";
2 |
3 | const auth = (state = initialState, action) => {
4 | switch (action.type) {
5 | case 'SIGNUP_USER_SUCCESS':
6 | console.log(action)
7 | return { ...state, user: action.payload.user, isAuthenticated: true };
8 | case 'SIGNUP_USER_FAILURE':
9 | console.log(action)
10 | return { ...state, signUpErr: action.payload.error };
11 |
12 | case 'SET_CURRENT_USER_SUCCESS':
13 | return { ...state, user: action.payload };
14 |
15 | // Sign in cases
16 | case 'SIGNIN_USER_SUCCESS':
17 | console.log(action)
18 | return { ...state, user: action.payload.user, isAuthenticated: true };
19 | case 'SIGNIN_USER_FAILURE':
20 | console.log(action);
21 | return { ...state, signInErr: action.payload.error }
22 | default:
23 | return state;
24 | }
25 | }
26 |
27 | export default auth;
28 |
--------------------------------------------------------------------------------
/client/src/reducers/todos.js:
--------------------------------------------------------------------------------
1 | import { initialState } from "../context/todosContext";
2 |
3 | const todosReducer = (state = initialState, action) => {
4 | switch (action.type) {
5 | case 'CREATE_TODO_LOADING':
6 | return { ...state };
7 | case 'CREATE_TODO_SUCCESS':
8 | return {
9 | ...state,
10 | todos: [action.payload, ...state.todos]
11 | };
12 | case 'CREATE_TODO_FAILURE':
13 | return {
14 | ...state,
15 | createTodoError: action.payload.error
16 | };
17 |
18 | case 'FETCH_TODOS_LOADING':
19 | return {
20 | ...state,
21 | };
22 | case 'FETCH_TODOS_SUCCESS':
23 | console.log(action)
24 | return {
25 | ...state,
26 | todos: action.payload,
27 | };
28 | case 'FETCH_TODOS_FAILURE':
29 | return {
30 | ...state,
31 | fetchTodosError: action.error,
32 | };
33 |
34 | case 'DELETE_TODO_SUCCESS':
35 | const { todos } = state;
36 | const newTodos = todos.filter((todo) => todo.id !== action.payload);
37 | return { ...state, todos: newTodos };
38 |
39 | case 'CREATE_TASK_SUCCESS':
40 | console.log(action.payload)
41 | return { ...state };
42 |
43 | default:
44 | return state;
45 | }
46 | }
47 |
48 | export default todosReducer;
49 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "clean": "rm -rf build && mkdir build",
8 | "build": "npm run clean && babel api --out-dir build",
9 | "start": "node build/app.js",
10 | "dev": "nodemon --exec babel-node api/app.js",
11 | "migrate:all": "sequelize db:migrate:undo:all && sequelize db:migrate"
12 | },
13 | "keywords": [],
14 | "author": "",
15 | "license": "ISC",
16 | "devDependencies": {
17 | "@babel/cli": "^7.7.5",
18 | "@babel/core": "^7.7.5",
19 | "@babel/node": "^7.7.4",
20 | "@babel/preset-env": "^7.7.5",
21 | "@babel/preset-react": "^7.7.4",
22 | "babel-eslint": "^10.0.3",
23 | "eslint": "^6.7.2",
24 | "eslint-config-airbnb-base": "^14.0.0",
25 | "eslint-plugin-import": "^2.18.2",
26 | "eslint-plugin-react": "^7.17.0",
27 | "pg": "^7.17.1"
28 | },
29 | "dependencies": {
30 | "@sendgrid/mail": "^6.5.1",
31 | "bcryptjs": "^2.4.3",
32 | "body-parser": "^1.19.0",
33 | "classnames": "^2.2.6",
34 | "cors": "^2.8.5",
35 | "dotenv": "^8.2.0",
36 | "express": "^4.17.1",
37 | "js-cookie": "^2.2.1",
38 | "jsonwebtoken": "^8.5.1",
39 | "jwt-decode": "^2.2.0",
40 | "react-router-dom": "^5.1.2",
41 | "sequelize": "^5.21.2",
42 | "validator": "^12.2.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------