<repo_name>termaik1/task-FrameWork-Team<file_sep>/client/src/store/reducers.test.js
import reducers from "./reducers";
import actions from "./actions";

describe("Test reducers in Todo", () => {
  let state;
  beforeEach(() => {
    state = {
      origin: [
        {
          _id: 1,
          name: "test1",
          completed: false,
        },
        {
          _id: 2,
          name: "test2",
          completed: true,
        },
      ],
    };
  });

  it("add items", () => {
    const data = [
      {
        _id: 1,
        name: "test1",
        completed: false,
      },
      {
        _id: 2,
        name: "test2",
        completed: true,
      },
    ];
    const action = actions.getSuccess(data);

    const newState = reducers({ origin: [] }, action);
    expect(newState.origin.length).toBe(2);
  });

  it("item addition", () => {
    const action = actions.createSuccess({
      _id: 3,
      name: "test3",
      completed: true,
    });

    const newState = reducers(state, action);
    expect(newState.origin.length).toBe(3);
  });

  it("item delete", () => {
    const action = actions.deleteSuccess({
      _id: 2,
      name: "test2",
      completed: true,
    });

    const newState = reducers(state, action);
    expect(newState.origin.length).toBe(1);
  });
});
<file_sep>/server/src/cors/db.ts
import mongoose from "mongoose";

export default () =>
  mongoose.connect(
    "mongodb+srv://admin:<EMAIL>@cluster0.cqaze.mongodb.net/Todo?retryWrites=true&w=majority",
    { useNewUrlParser: true, useCreateIndex: true, useFindAndModify: true }
  );<file_sep>/server/src/index.ts
import express from "express";
import bodyParser from 'body-parser'
import dotenv from 'dotenv'
import http from 'http'
import cors from 'cors'

import { db, createRoutes } from './cors'

const app = express();

const httpServer = http.createServer(app)

dotenv.config()

db()

app.use(bodyParser.json())
app.use(cors())

createRoutes(app)

httpServer.listen(process.env.PORT, () =>
  console.log(`Server start http://localhost:${process.env.PORT}`)
);<file_sep>/server/src/cors/index.ts
export { default as createRoutes } from './createRoutes'
export { default as db } from './db'<file_sep>/client/src/Item.js
import React from "react";

import PropTypes from "prop-types";

import {
  Checkbox,
  TextField,
  FormGroup,
  FormControlLabel,
} from "@material-ui/core";

import DeleteIcon from "@material-ui/icons/Delete";

const Item = (props) => {
  const { onChange, onDelete, _id, completed, name } = props;

  return (
    <FormGroup row>
      <FormControlLabel
        control={
          <Checkbox
            checked={completed}
            name="checkedA"
            onChange={(e) => onChange(_id, e.target.checked, name)}
          />
        }
      />
      <FormControlLabel
        control={
          <TextField
            id="standard-form-text"
            defaultValue={name}
            onChange={(e) => onChange(_id, completed, e.target.value)}
          />
        }
      />
      <FormControlLabel
        control={<DeleteIcon onClick={() => onDelete(_id)} />}
      />
    </FormGroup>
  );
};

Item.propTypes = {
  onChange: PropTypes.func,
  onDelete: PropTypes.func,
  _id: PropTypes.number,
  completed: PropTypes.bool,
  name: PropTypes.string,
};

export default Item;
<file_sep>/client/src/List.js
import React, { useMemo, useCallback } from "react";
import { useDispatch } from "react-redux";

import _ from "lodash";

import todoActions from "./store/actions";

import Item from "./Item";

const List = (props) => {
  const dispatch = useDispatch();

  const { origin } = props;

  const onDelete = useCallback((_id) => dispatch(todoActions.delete(_id)), []);

  const onChange = useCallback(
    (_id, completed, name) =>
      dispatch(todoActions.edit({ _id, completed, name })),
    []
  );

  const renderItem = useMemo(
    () =>
      !!_.size(origin) &&
      _.map(origin, (item) => (
        <Item
          key={item._id}
          {...item}
          onChange={onChange}
          onDelete={onDelete}
        />
      )),
    [origin]
  );

  return renderItem;
};

export default List;
<file_sep>/client/src/store/store.js
import { createStore, applyMiddleware, combineReducers } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { composeWithDevTools } from 'redux-devtools-extension';

import Todo from './reducers';
import todoSagas from './sagas';

const sagaMiddleware = createSagaMiddleware();

const middlewares = [sagaMiddleware];

const rootReducers = combineReducers({
  Todo,
});


const store = createStore(
  rootReducers,
  composeWithDevTools(applyMiddleware(...middlewares))
);

sagaMiddleware.run(todoSagas);

export default store;
<file_sep>/README.md
# 1)Выполнил: 
	<NAME>

# 2) Описание
 
### frontend(ReactJS) 
 
  было реализован функционал согласно ТЗ
  + были добавлены unit тесты на jest и enzyme

### backend(NodeJS) 
  
  созданы методы CRUD  


# 3) Настройка:
 	 на стороне fronted запустить команду:
  	      * 1) npm install
 
	на стороне backend запустить команды:
  	      * 1) npm install
  		
  **Возможно не будет коннект с mongoDB, для это не обходимо сделать следующее:
   * 1) залогиниться на сайте https://account.mongodb.com/ 
   * 2) Авторизоваться по гугл аккаунту(ниже прикреплен)
   * 3) Зайти на вкладку "Network Access"
   * 4) Кликнуть на "+ ADD IP ADDRESS", после нажать на "ADD CURRENT IP ADDRESS" и сохранить "confirm"
   * 5) Подождать пару минут

# 4) Аккаунт
	google аккаунт:
		user: <EMAIL>
		password: <PASSWORD>

# 5) Запуск проекта:

на стороне frontend выполнить команду : 
  
  * npm run start

так же есть команда "test" для запуска unit тестов

  * npm run test

на стороне backend выполнить команду : 
  
  * npm run start

<file_sep>/server/src/controllers/types.ts
export function getObjValue<T extends object, R extends keyof T>(
  obj: T,
  key: R
) {
  return obj[key];
}
<file_sep>/client/src/store/constants.js
export default {
  GET:               'SERVER:GET',
  GET_SUCCESS:       'GET_SUCCESS',
  GET_FAILURE:       'GET_FAILURE',

  CREATE:            'SERVER:CREATE',
  CREATE_SUCCESS:    'CREATE_SUCCESS',
  CREATE_FAILURE:    'CREATE_FAILURE',

  EDIT:              'SERVER:EDIT',
  EDIT_SUCCESS:      'EDIT_SUCCESS',
  EDIT_FALURE:       'EDIT_FAILURE',

  DELETE:            'SERVER:DELETE',
  DELETE_SUCCESS:    'DELETE_SUCCESS',
  DELETE_FALURE:     'DELETE_FALURE'
};
<file_sep>/server/src/controllers/index.ts
export { default as TodoController } from "./TodoController";
<file_sep>/client/src/List.test.js
import React from "react";

import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import { create } from "react-test-renderer";

import List from "./List";

configure({ adapter: new Adapter() });
jest.mock("react-redux", () => ({
  ...jest.requireActual("react-redux"),
  useDispatch: jest.fn,
}));

describe("List component", () => {
  let props;
  beforeEach(() => {
    props = {
      origin: [
        {
          _id: 1,
          name: "test1",
          completed: false,
        },
        {
          _id: 2,
          name: "test2",
          completed: true,
        },
      ],
    };
  });

  it("Transfer check props", () => {
    const component = create(<List {...props} />);
    const root = component.root;

    const rootComponent = root.findByProps(props);

    expect(rootComponent.props).toEqual(props);
  });
});
<file_sep>/server/src/models/Todo.ts
import { Schema, model, Document } from "mongoose";

export interface ITodo extends Document {
  _id: string;
  name: string;
  completed: boolean;
}

const TodoSchema = new Schema({
  name: String,
  completed: {
    type: Boolean,
    default: false,
  },
});

const TodoModel = model<ITodo>("Todo", TodoSchema);

export default TodoModel;
<file_sep>/client/src/store/reducers.js
import _ from "lodash";
import types from "./constants";

const initialState = {
  origin: [],
};

const TodoReducers = {
  [types.GET_SUCCESS]: (state, action) => ({
    ...state,
    origin: action.payload,
  }),
  [types.CREATE_SUCCESS]: (state, action) => {
    const newTodo = action.payload;
    const prevOrigin = [...state.origin];

    return {
      ...state,
      origin: [...prevOrigin, newTodo],
    };
  },
  [types.DELETE_SUCCESS]: (state, action) => {
    const deleteTodo = action.payload;
    const prevOrigin = [...state.origin];
    const newOrigin = _.filter(
      prevOrigin,
      (item) => item._id !== deleteTodo._id
    );

    return { ...state, origin: [...newOrigin] };
  },
};

const Todo = (state = initialState, action) => {
  const reducers = TodoReducers[action.type];

  if (!reducers) {
    return state;
  }
  return reducers(state, action);
};

export default Todo;
<file_sep>/client/src/App.js
import React, { useEffect, useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import todoActions from './store/actions';

import { TextField, Button } from '@material-ui/core/';

import List from './List';

const App = () => {
  const dispatch = useDispatch();

  const [name, setName] = useState("");

  const { origin } = useSelector((state) => state.Todo);
  useEffect(() => {
    dispatch(todoActions.get());
  }, []);

  const onCreate = useCallback(() => {
    dispatch(todoActions.create({ name }));
    setName("");
  }, [name]);

  return (
    <div className="app">
      <div className="app__container">
        <div className="app__container-panel">
          <TextField
            id="outlined-required"
            label="Task"
            value={name}
            variant="outlined"
            size="small"
            onChange={(e) => setName(e.target.value)}
          />
          <Button
            id="button-material"
            size="large"
            variant="outlined"
            onClick={onCreate}
          >
            ADD
          </Button>
        </div>
        <div className="app__container-content">
          <List origin={origin} />
        </div>
      </div>
    </div>
  );
};

export default App;
<file_sep>/client/src/Item.test.js
import React from "react";

import { create } from "react-test-renderer";

import Item from "./Item";

describe("Item component", () => {
  let props;
  beforeEach(() => {
    props = {
      _id: 1,
      name: "Test1",
      completed: true,
    };
  });

  it("check transmission props", () => {
    const component = create(<Item {...props} />);

    const root = component.root;
    const rootComponent = root.findByProps(props);

    expect(rootComponent.props).toEqual(props);
  });
});
<file_sep>/server/src/controllers/TodoController.ts
import express from "express";
import _ from "lodash";
import { getObjValue } from "./types";
import TodoModel from "../models/Todo";
import { ITodo } from "../models/Todo";

class TodoController {
  get = async (req: express.Request, res: express.Response) => {
    try {
      const todos: Array<ITodo> = await TodoModel.find();

      res.json(todos);
    } catch (err) {
      console.log("ERR", err);
      res.status(400).json({ message: "Error" });
    }
  };
  create = async (req: express.Request, res: express.Response) => {
    try {
      const name: string = getObjValue(req.body, "name");

      const todoCreate = new TodoModel({ name });
      const todo = await todoCreate.save();
      res.json(todo);
    } catch (err) {
      console.log("ERR", err);
      res.status(400).json({ message: "Error" });
    }
  };
  patch = async (req: express.Request, res: express.Response) => {
    try {
      const _id: string = getObjValue(req.body, "_id");
      const name: string = getObjValue(req.body, "name");
      const completed: boolean = getObjValue(req.body, "completed");

      const todo: ITodo = await TodoModel.findOneAndUpdate(
        { _id },
        { name, completed },
        {
          new: true,
          upsert: true,
        }
      );

      res.json(todo);
    } catch (err) {
      console.log("ERR", err);
      res.status(400).json({ message: "Error" });
    }
  };
  delete = async (req: express.Request, res: express.Response) => {
    try {
      const _id: string = getObjValue(req.params, "id");

      const todoDelete = await TodoModel.findByIdAndRemove({ _id });

      res.json(todoDelete);
    } catch (err) {
      console.log("ERR", err);
      res.status(400).json({ message: "Error" });
    }
  };
}

export default TodoController;
<file_sep>/client/src/store/actions.js
import types from "./constants";

const actions = {
  get: () => ({
    type: types.GET,
  }),
  getSuccess: (data) => ({
    type: types.GET_SUCCESS,
    payload: data,
  }),
  getFailure: () => ({
    type: types.GET_FAILURE,
  }),

  create: (data) => ({
    type: types.CREATE,
    payload: data,
  }),
  createSuccess: (data) => ({
    type: types.CREATE_SUCCESS,
    payload: data,
  }),
  createFailure: () => ({
    type: types.CREATE_FAILURE,
  }),

  edit: (data) => ({
    type: types.EDIT,
    payload: data,
  }),
  editSuccess: (data) => ({
    type: types.EDIT_SUCCESS,
    payload: data,
  }),
  editFailure: () => ({
    type: types.EDIT_FAILURE,
  }),

  delete: (data) => ({
    type: types.DELETE,
    payload: data,
  }),
  deleteSuccess: (data) => ({
    type: types.DELETE_SUCCESS,
    payload: data,
  }),
  deleteFailure: () => ({
    type: types.DELETE_FAILURE,
  }),
};

export default actions;
<file_sep>/client/src/App.test.js
import React from "react";
import { shallow, configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";

import App from "./App";
configure({ adapter: new Adapter() });
jest.mock("react-redux", () => ({
  ...jest.requireActual("react-redux"),
  useSelector: jest.fn,
  useDispatch: jest.fn,
}));

describe("App component", () => {
  let component;
  beforeEach(() => {
    component = shallow(<App />);
  });

  it("checking the rendering of all blocks <div>", () => {
    const divAll = component.find("div");
    const divOne = component.find(".app");
    const divTwo = component.find(".app__container");
    const divThree = component.find(".app__container-panel");
    const divFour = component.find(".app__container-content");

    expect(divAll).toHaveLength(4);
    expect(divOne).toHaveLength(1);
    expect(divTwo).toHaveLength(1);
    expect(divThree).toHaveLength(1);
    expect(divFour).toHaveLength(1);
  });

  it("checking rendering of <TextField> from library 'material-ui' by its #id", () => {
    const textFieldId = component.find("#outlined-required");

    expect(textFieldId).toHaveLength(1);
  });
  it("checking rendering of <Button> from library 'material-ui' by its #id", () => {
    const buttonId = component.find("#button-material");
    const buttonText = buttonId.text();
    expect(buttonId).toHaveLength(1);
    expect(buttonText).toBe("ADD");
  });
  it("Check the rendering of a custom component <List>", () => {
    const list = component.find("List");

    expect(list).toHaveLength(1);
  });
});
<file_sep>/server/src/cors/createRoutes.ts
import TodoController from "../controllers/TodoController";

export default (app: any) => {
  const Todo = new TodoController();

  app.get("/todos", Todo.get);
  app.post("/create", Todo.create);
  app.patch("/edit", Todo.patch);
  app.delete("/delete/:id", Todo.delete);
};
