From dfde175c07b70cc1d3f6e1c856376ac13125a9e8 Mon Sep 17 00:00:00 2001 From: canove Date: Wed, 2 Sep 2020 21:01:02 -0300 Subject: [PATCH] feat: added users page --- backend/package.json | 4 +- backend/src/app.js | 1 - backend/src/controllers/ContactController.js | 8 +- backend/src/controllers/TicketController.js | 2 +- backend/src/controllers/UserController.js | 108 +++++- ...00901235509-add-profile-column-to-users.js | 15 + backend/src/middleware/is-auth.js | 28 +- backend/src/models/User.js | 1 + backend/src/routes/auth.js | 3 + backend/src/routes/users.js | 23 +- backend/src/services/wbotMessageListener.js | 2 +- backend/yarn.lock | 65 +++- frontend/package.json | 3 +- frontend/src/App.js | 12 +- .../src/components/ContactDrawer/index.js | 20 +- frontend/src/components/ContactModal/index.js | 88 ++--- .../src/components/ContactsSekeleton/index.js | 163 -------- .../src/components/MainContainer/index.js | 31 ++ frontend/src/components/MainHeader/index.js | 19 + .../MainHeaderButtonsWrapper/index.js | 21 ++ frontend/src/components/MessagesList/index.js | 10 +- .../components/NotificationsPopOver/index.js | 9 +- .../src/components/PaginationActions/index.js | 71 ---- .../src/components/TableRowSkeleton/index.js | 78 ++++ frontend/src/components/TicketsList/index.js | 9 +- frontend/src/components/Title/index.js | 10 + frontend/src/components/UserModal/index.js | 212 +++++++++++ .../src/components/_layout/MainListItems.js | 4 +- frontend/src/pages/Contacts/index.js | 356 ++++++++---------- frontend/src/pages/Dashboard/Title.js | 6 +- frontend/src/pages/Signup/index.js | 162 ++++---- frontend/src/pages/Users/index.js | 234 ++++++++++++ frontend/src/routes/index.js | 2 + 33 files changed, 1121 insertions(+), 659 deletions(-) create mode 100644 backend/src/database/migrations/20200901235509-add-profile-column-to-users.js delete mode 100644 frontend/src/components/ContactsSekeleton/index.js create mode 100644 frontend/src/components/MainContainer/index.js create mode 100644 frontend/src/components/MainHeader/index.js create mode 100644 frontend/src/components/MainHeaderButtonsWrapper/index.js delete mode 100644 frontend/src/components/PaginationActions/index.js create mode 100644 frontend/src/components/TableRowSkeleton/index.js create mode 100644 frontend/src/components/Title/index.js create mode 100644 frontend/src/components/UserModal/index.js create mode 100644 frontend/src/pages/Users/index.js diff --git a/backend/package.json b/backend/package.json index 9bc51aa..cfe437b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -22,7 +22,6 @@ "dotenv": "^8.2.0", "express": "^4.17.1", "express-async-errors": "^3.1.1", - "express-validator": "^6.5.0", "jsonwebtoken": "^8.5.1", "multer": "^1.4.2", "mysql2": "^2.1.0", @@ -30,7 +29,8 @@ "sequelize": "^6.3.4", "socket.io": "^2.3.0", "whatsapp-web.js": "^1.8.0", - "youch": "^2.0.10" + "youch": "^2.0.10", + "yup": "^0.29.3" }, "devDependencies": { "nodemon": "^2.0.4", diff --git a/backend/src/app.js b/backend/src/app.js index 273d65a..f743ec9 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -82,6 +82,5 @@ app.use(async (err, req, res, next) => { console.log(err); return res.status(500).json(errors); } - console.log(err); return res.status(500).json({ error: "Internal server error" }); }); diff --git a/backend/src/controllers/ContactController.js b/backend/src/controllers/ContactController.js index 2fc503a..3ee343b 100644 --- a/backend/src/controllers/ContactController.js +++ b/backend/src/controllers/ContactController.js @@ -8,7 +8,7 @@ const { getIO } = require("../libs/socket"); const { getWbot } = require("../libs/wbot"); exports.index = async (req, res) => { - const { searchParam = "", pageNumber = 1, rowsPerPage = 10 } = req.query; + const { searchParam = "", pageNumber = 1 } = req.query; const whereCondition = { [Op.or]: [ @@ -23,7 +23,7 @@ exports.index = async (req, res) => { ], }; - let limit = +rowsPerPage; + let limit = 20; let offset = limit * (pageNumber - 1); const { count, rows: contacts } = await Contact.findAndCountAll({ @@ -33,7 +33,9 @@ exports.index = async (req, res) => { order: [["createdAt", "DESC"]], }); - return res.json({ contacts, count }); + const hasMore = count > offset + contacts.length; + + return res.json({ contacts, count, hasMore }); }; exports.store = async (req, res) => { diff --git a/backend/src/controllers/TicketController.js b/backend/src/controllers/TicketController.js index 5ae04ab..fa40be8 100644 --- a/backend/src/controllers/TicketController.js +++ b/backend/src/controllers/TicketController.js @@ -16,7 +16,7 @@ exports.index = async (req, res) => { showAll, } = req.query; - const userId = req.userId; + const userId = req.user.id; const limit = 20; const offset = limit * (pageNumber - 1); diff --git a/backend/src/controllers/UserController.js b/backend/src/controllers/UserController.js index fbbb94b..771d2b2 100644 --- a/backend/src/controllers/UserController.js +++ b/backend/src/controllers/UserController.js @@ -1,34 +1,101 @@ -const { validationResult } = require("express-validator"); +const Sequelize = require("sequelize"); +const Yup = require("yup"); +const { Op } = require("sequelize"); const User = require("../models/User"); +const { getIO } = require("../libs/socket"); + exports.index = async (req, res) => { - // const { searchParam = "", pageNumber = 1 } = req.query; + const { searchParam = "", pageNumber = 1, rowsPerPage = 10 } = req.query; - const users = await User.findAll({ attributes: ["name", "id", "email"] }); + const whereCondition = { + [Op.or]: [ + { + name: Sequelize.where( + Sequelize.fn("LOWER", Sequelize.col("name")), + "LIKE", + "%" + searchParam.toLowerCase() + "%" + ), + }, + { email: { [Op.like]: `%${searchParam.toLowerCase()}%` } }, + ], + }; - return res.status(200).json(users); + let limit = +rowsPerPage; + let offset = limit * (pageNumber - 1); + + const { count, rows: users } = await User.findAndCountAll({ + attributes: ["name", "id", "email", "profile"], + where: whereCondition, + limit, + offset, + order: [["createdAt", "DESC"]], + }); + + return res.status(200).json({ users, count }); }; exports.store = async (req, res, next) => { - const errors = validationResult(req); + const schema = Yup.object().shape({ + name: Yup.string().required().min(2), + email: Yup.string() + .email() + .required() + .test( + "Check-email", + "An user with this email already exists", + async value => { + const userFound = await User.findOne({ where: { email: value } }); + return !Boolean(userFound); + } + ), + password: Yup.string().required().min(5), + }); - if (!errors.isEmpty()) { - return res - .status(400) - .json({ error: "Validation failed", data: errors.array() }); - } + await schema.validate(req.body); - const { name, id, email } = await User.create(req.body); + const io = getIO(); - res.status(201).json({ message: "User created!", userId: id }); + const { name, id, email, profile } = await User.create(req.body); + + io.emit("user", { + action: "create", + user: { name, id, email, profile }, + }); + + return res.status(201).json({ message: "User created!", userId: id }); +}; + +exports.show = async (req, res) => { + const { userId } = req.params; + + const { id, name, email, profile } = await User.findByPk(userId); + + return res.status(200).json({ + id, + name, + email, + profile, + }); }; exports.update = async (req, res) => { + const schema = Yup.object().shape({ + name: Yup.string().min(2), + email: Yup.string().email(), + password: Yup.string(), + }); + + console.log("cai aqui"); + + await schema.validate(req.body); + + const io = getIO(); const { userId } = req.params; const user = await User.findByPk(userId, { - attributes: ["name", "id", "email"], + attributes: ["name", "id", "email", "profile"], }); if (!user) { @@ -37,12 +104,16 @@ exports.update = async (req, res) => { await user.update(req.body); - //todo, send socket IO to users channel. + io.emit("user", { + action: "update", + user: user, + }); - res.status(200).json(user); + return res.status(200).json(user); }; exports.delete = async (req, res) => { + const io = getIO(); const { userId } = req.params; const user = await User.findByPk(userId); @@ -53,5 +124,10 @@ exports.delete = async (req, res) => { await user.destroy(); - res.status(200).json({ message: "User deleted" }); + io.emit("user", { + action: "delete", + userId: userId, + }); + + return res.status(200).json({ message: "User deleted" }); }; diff --git a/backend/src/database/migrations/20200901235509-add-profile-column-to-users.js b/backend/src/database/migrations/20200901235509-add-profile-column-to-users.js new file mode 100644 index 0000000..cb3e93b --- /dev/null +++ b/backend/src/database/migrations/20200901235509-add-profile-column-to-users.js @@ -0,0 +1,15 @@ +"use strict"; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.addColumn("Users", "profile", { + type: Sequelize.STRING, + allowNull: false, + defaultValue: "admin", + }); + }, + + down: queryInterface => { + return queryInterface.removeColumn("Users", "profile"); + }, +}; diff --git a/backend/src/middleware/is-auth.js b/backend/src/middleware/is-auth.js index 6bfaabc..596d737 100644 --- a/backend/src/middleware/is-auth.js +++ b/backend/src/middleware/is-auth.js @@ -1,5 +1,7 @@ const jwt = require("jsonwebtoken"); +const util = require("util"); +const User = require("../models/User"); const authConfig = require("../config/auth"); module.exports = async (req, res, next) => { @@ -11,12 +13,24 @@ module.exports = async (req, res, next) => { const [, token] = authHeader.split(" "); - jwt.verify(token, authConfig.secret, (error, result) => { - if (error) { - return res.status(401).json({ error: "Invalid token" }); + try { + const decoded = await util.promisify(jwt.verify)(token, authConfig.secret); + + const user = await User.findByPk(decoded.userId, { + attributes: ["id", "name", "profile", "email"], + }); + + if (!user) { + return res + .status(401) + .json({ error: "The token corresponding user does not exists." }); } - req.userId = result.userId; - // todo >> find user in DB and store in req.user to use latter, or throw an error if user not exists anymore - next(); - }); + + req.user = user; + + return next(); + } catch (err) { + console.log(err); + return res.status(401).json({ error: "Invalid Token" }); + } }; diff --git a/backend/src/models/User.js b/backend/src/models/User.js index d474366..375f129 100644 --- a/backend/src/models/User.js +++ b/backend/src/models/User.js @@ -7,6 +7,7 @@ class User extends Sequelize.Model { { name: { type: Sequelize.STRING }, password: { type: Sequelize.VIRTUAL }, + profile: { type: Sequelize.STRING, defaultValue: "admin" }, passwordHash: { type: Sequelize.STRING }, email: { type: Sequelize.STRING }, }, diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index 7d0e52d..28a944e 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -1,9 +1,12 @@ const express = require("express"); const SessionController = require("../controllers/SessionController"); +const UserController = require("../controllers/UserController"); const isAuth = require("../middleware/is-auth"); const routes = express.Router(); +routes.post("/signup", UserController.store); + routes.post("/login", SessionController.store); routes.get("/check", isAuth, (req, res) => { diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index 7e6d46d..bbdb0de 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -1,5 +1,4 @@ const express = require("express"); -const { body } = require("express-validator"); const User = require("../models/User"); const isAuth = require("../middleware/is-auth"); @@ -9,28 +8,12 @@ const routes = express.Router(); routes.get("/users", isAuth, UserController.index); -routes.post( - "/users", - [ - body("email") - .isEmail() - .withMessage("Email inválido") - .custom((value, { req }) => { - return User.findOne({ where: { email: value } }).then(user => { - if (user) { - return Promise.reject("An user with this email already exists!"); - } - }); - }) - .normalizeEmail(), - body("password").trim().isLength({ min: 5 }), - body("name").trim().not().isEmpty(), - ], - UserController.store -); +routes.post("/users", isAuth, UserController.store); routes.put("/users/:userId", isAuth, UserController.update); +routes.get("/users/:userId", isAuth, UserController.show); + routes.delete("/users/:userId", isAuth, UserController.delete); module.exports = routes; diff --git a/backend/src/services/wbotMessageListener.js b/backend/src/services/wbotMessageListener.js index 0162838..329640b 100644 --- a/backend/src/services/wbotMessageListener.js +++ b/backend/src/services/wbotMessageListener.js @@ -137,7 +137,7 @@ const wbotMessageListener = () => { const io = getIO(); wbot.on("message_create", async msg => { - // console.log(msg); + console.log(msg); if ( msg.from === "status@broadcast" || msg.type === "location" || diff --git a/backend/yarn.lock b/backend/yarn.lock index 0deb2e7..be20bb0 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -2,6 +2,13 @@ # yarn lockfile v1 +"@babel/runtime@^7.10.5": + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" + integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== + dependencies: + regenerator-runtime "^0.13.4" + "@pedroslopez/moduleraid@^4.1.0": version "4.1.0" resolved "https://registry.yarnpkg.com/@pedroslopez/moduleraid/-/moduleraid-4.1.0.tgz#468f7195fddc9f367e672ace9269f0698cf4c404" @@ -854,14 +861,6 @@ express-async-errors@^3.1.1: resolved "https://registry.yarnpkg.com/express-async-errors/-/express-async-errors-3.1.1.tgz#6053236d61d21ddef4892d6bd1d736889fc9da41" integrity sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng== -express-validator@^6.5.0: - version "6.6.1" - resolved "https://registry.yarnpkg.com/express-validator/-/express-validator-6.6.1.tgz#c53046f615d27fcb78be786e018dcd60bd9c6c5c" - integrity sha512-+MrZKJ3eGYXkNF9p9Zf7MS7NkPJFg9MDYATU5c80Cf4F62JdLBIjWxy6481tRC0y1NnC9cgOw8FuN364bWaGhA== - dependencies: - lodash "^4.17.19" - validator "^13.1.1" - express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -950,6 +949,11 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" +fn-name@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-3.0.0.tgz#0596707f635929634d791f452309ab41558e3c5c" + integrity sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA== + forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" @@ -1382,6 +1386,11 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" +lodash-es@^4.17.11: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78" + integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ== + lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -1417,7 +1426,7 @@ lodash.once@^4.0.0: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= -lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.5: +lodash@^4.17.15, lodash@^4.17.5: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -1801,6 +1810,11 @@ progress@^2.0.1: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +property-expr@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.4.tgz#37b925478e58965031bb612ec5b3260f8241e910" + integrity sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg== + proto-list@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" @@ -1941,6 +1955,11 @@ redeyed@~2.1.0: dependencies: esprima "~4.0.0" +regenerator-runtime@^0.13.4: + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== + registry-auth-token@^4.0.0: version "4.2.0" resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.0.tgz#1d37dffda72bbecd0f581e4715540213a65eb7da" @@ -2266,6 +2285,11 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +synchronous-promise@^2.0.13: + version "2.0.13" + resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.13.tgz#9d8c165ddee69c5a6542862b405bc50095926702" + integrity sha512-R9N6uDkVsghHePKh1TEqbnLddO2IY25OcsksyFp/qBe7XYd0PVbKEWxhcdMhpLzE1I6skj5l4aEZ3CRxcbArlA== + tar-fs@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5" @@ -2332,6 +2356,11 @@ toposort-class@^1.0.1: resolved "https://registry.yarnpkg.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988" integrity sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg= +toposort@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" + integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA= + touch@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" @@ -2464,11 +2493,6 @@ validator@^10.11.0: resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228" integrity sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw== -validator@^13.1.1: - version "13.1.1" - resolved "https://registry.yarnpkg.com/validator/-/validator-13.1.1.tgz#f8811368473d2173a9d8611572b58c5783f223bf" - integrity sha512-8GfPiwzzRoWTg7OV1zva1KvrSemuMkv07MA9TTl91hfhe+wKrsrgVN4H2QSFd/U/FhiU3iWPYVgvbsOGwhyFWw== - vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -2614,3 +2638,16 @@ youch@^2.0.10: cookie "^0.3.1" mustache "^3.0.0" stack-trace "0.0.10" + +yup@^0.29.3: + version "0.29.3" + resolved "https://registry.yarnpkg.com/yup/-/yup-0.29.3.tgz#69a30fd3f1c19f5d9e31b1cf1c2b851ce8045fea" + integrity sha512-RNUGiZ/sQ37CkhzKFoedkeMfJM0vNQyaz+wRZJzxdKE7VfDeVKH8bb4rr7XhRLbHJz5hSjoDNwMEIaKhuMZ8gQ== + dependencies: + "@babel/runtime" "^7.10.5" + fn-name "~3.0.0" + lodash "^4.17.15" + lodash-es "^4.17.11" + property-expr "^2.0.2" + synchronous-promise "^2.0.13" + toposort "^2.0.2" diff --git a/frontend/package.json b/frontend/package.json index f49fa02..1102b93 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,7 +25,8 @@ "react-scripts": "3.4.1", "react-toastify": "^6.0.8", "recharts": "^1.8.5", - "socket.io-client": "^2.3.0" + "socket.io-client": "^2.3.0", + "yup": "^0.29.3" }, "scripts": { "start": "react-scripts start", diff --git a/frontend/src/App.js b/frontend/src/App.js index cb42d18..f1dfbde 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -10,8 +10,18 @@ const App = () => { const theme = createMuiTheme( { + scrollbarStyles: { + "&::-webkit-scrollbar": { + width: "8px", + height: "8px", + }, + "&::-webkit-scrollbar-thumb": { + boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", + backgroundColor: "#e8e8e8", + }, + }, palette: { - primary: { main: "#1976d2" }, + primary: { main: "#2576d2" }, }, }, locale diff --git a/frontend/src/components/ContactDrawer/index.js b/frontend/src/components/ContactDrawer/index.js index d0b0b3d..da512bf 100644 --- a/frontend/src/components/ContactDrawer/index.js +++ b/frontend/src/components/ContactDrawer/index.js @@ -48,14 +48,7 @@ const useStyles = makeStyles(theme => ({ padding: "8px 0px 8px 8px", height: "100%", overflowY: "scroll", - "&::-webkit-scrollbar": { - width: "8px", - height: "8px", - }, - "&::-webkit-scrollbar-thumb": { - boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", - backgroundColor: "#e8e8e8", - }, + ...theme.scrollbarStyles, }, contactAvatar: { @@ -80,17 +73,6 @@ const useStyles = makeStyles(theme => ({ padding: 8, display: "flex", flexDirection: "column", - // overflowX: "scroll", - // flex: 1, - // "&::-webkit-scrollbar": { - // width: "8px", - // height: "8px", - // }, - // "&::-webkit-scrollbar-thumb": { - // // borderRadius: "2px", - // boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", - // backgroundColor: "#e8e8e8", - // }, }, contactExtraInfo: { marginTop: 4, diff --git a/frontend/src/components/ContactModal/index.js b/frontend/src/components/ContactModal/index.js index 1270e71..1d5a0d3 100644 --- a/frontend/src/components/ContactModal/index.js +++ b/frontend/src/components/ContactModal/index.js @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; -import { Formik, FieldArray } from "formik"; +import * as Yup from "yup"; +import { Formik, FieldArray, Form, Field } from "formik"; import { makeStyles } from "@material-ui/core/styles"; import { green } from "@material-ui/core/colors"; @@ -52,6 +53,15 @@ const useStyles = makeStyles(theme => ({ }, })); +const ContactSchema = Yup.object().shape({ + name: Yup.string() + .min(2, "Too Short!") + .max(50, "Too Long!") + .required("Required"), + number: Yup.string().min(8, "Too Short!").max(50, "Too Long!"), + email: Yup.string().email("Invalid email"), +}); + const ContactModal = ({ open, onClose, contactId }) => { const classes = useStyles(); @@ -86,7 +96,7 @@ const ContactModal = ({ open, onClose, contactId }) => { await api.post("/contacts", values); } } catch (err) { - alert(err.response.data.error); + alert(JSON.stringify(err.response.data, null, 2)); console.log(err); } handleClose(); @@ -94,69 +104,57 @@ const ContactModal = ({ open, onClose, contactId }) => { return (
- + + + {contactId + ? `${i18n.t("contactModal.title.edit")}` + : `${i18n.t("contactModal.title.add")}`} + { + validationSchema={ContactSchema} + onSubmit={(values, actions) => { setTimeout(() => { handleSaveContact(values); - setSubmitting(false); + actions.setSubmitting(false); }, 400); }} > - {({ - values, - errors, - touched, - handleChange, - handleBlur, - handleSubmit, - isSubmitting, - }) => ( -
- - {contactId - ? `${i18n.t("contactModal.title.edit")}` - : `${i18n.t("contactModal.title.add")}`} - + {({ values, errors, touched, isSubmitting }) => ( + {i18n.t("contactModal.form.mainInfo")} - -
- { className={classes.extraAttr} key={`${index}-info`} > - - { )} - + )}
diff --git a/frontend/src/components/ContactsSekeleton/index.js b/frontend/src/components/ContactsSekeleton/index.js deleted file mode 100644 index fc5ffe6..0000000 --- a/frontend/src/components/ContactsSekeleton/index.js +++ /dev/null @@ -1,163 +0,0 @@ -import React from "react"; -import TableCell from "@material-ui/core/TableCell"; -import TableRow from "@material-ui/core/TableRow"; -import Skeleton from "@material-ui/lab/Skeleton"; - -const ContactsSekeleton = () => { - return ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export default ContactsSekeleton; diff --git a/frontend/src/components/MainContainer/index.js b/frontend/src/components/MainContainer/index.js new file mode 100644 index 0000000..24b7b9a --- /dev/null +++ b/frontend/src/components/MainContainer/index.js @@ -0,0 +1,31 @@ +import React from "react"; + +import { makeStyles } from "@material-ui/core/styles"; +import Container from "@material-ui/core/Container"; + +const useStyles = makeStyles(theme => ({ + mainContainer: { + flex: 1, + padding: theme.spacing(2), + height: `calc(100% - 48px)`, + }, + + contentWrapper: { + height: "100%", + overflowY: "hidden", + display: "flex", + flexDirection: "column", + }, +})); + +const MainContainer = ({ children }) => { + const classes = useStyles(); + + return ( + +
{children}
+
+ ); +}; + +export default MainContainer; diff --git a/frontend/src/components/MainHeader/index.js b/frontend/src/components/MainHeader/index.js new file mode 100644 index 0000000..46fa8ab --- /dev/null +++ b/frontend/src/components/MainHeader/index.js @@ -0,0 +1,19 @@ +import React from "react"; + +import { makeStyles } from "@material-ui/core/styles"; + +const useStyles = makeStyles(theme => ({ + contactsHeader: { + display: "flex", + alignItems: "center", + padding: "0px 6px 6px 6px", + }, +})); + +const MainHeader = ({ children }) => { + const classes = useStyles(); + + return
{children}
; +}; + +export default MainHeader; diff --git a/frontend/src/components/MainHeaderButtonsWrapper/index.js b/frontend/src/components/MainHeaderButtonsWrapper/index.js new file mode 100644 index 0000000..ed5887c --- /dev/null +++ b/frontend/src/components/MainHeaderButtonsWrapper/index.js @@ -0,0 +1,21 @@ +import React from "react"; + +import { makeStyles } from "@material-ui/core/styles"; + +const useStyles = makeStyles(theme => ({ + MainHeaderButtonsWrapper: { + flex: "none", + marginLeft: "auto", + "& > *": { + margin: theme.spacing(1), + }, + }, +})); + +const MainHeaderButtonsWrapper = ({ children }) => { + const classes = useStyles(); + + return
{children}
; +}; + +export default MainHeaderButtonsWrapper; diff --git a/frontend/src/components/MessagesList/index.js b/frontend/src/components/MessagesList/index.js index cd2b2ed..6e04f27 100644 --- a/frontend/src/components/MessagesList/index.js +++ b/frontend/src/components/MessagesList/index.js @@ -101,15 +101,7 @@ const useStyles = makeStyles(theme => ({ padding: "20px 20px 20px 20px", // scrollBehavior: "smooth", overflowY: "scroll", - "&::-webkit-scrollbar": { - width: "8px", - height: "8px", - }, - "&::-webkit-scrollbar-thumb": { - // borderRadius: "2px", - boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", - backgroundColor: "#e8e8e8", - }, + ...theme.scrollbarStyles, }, circleLoading: { diff --git a/frontend/src/components/NotificationsPopOver/index.js b/frontend/src/components/NotificationsPopOver/index.js index 5fadd84..a1234f3 100644 --- a/frontend/src/components/NotificationsPopOver/index.js +++ b/frontend/src/components/NotificationsPopOver/index.js @@ -24,14 +24,7 @@ const useStyles = makeStyles(theme => ({ tabContainer: { overflowY: "auto", maxHeight: 350, - "&::-webkit-scrollbar": { - width: "8px", - height: "8px", - }, - "&::-webkit-scrollbar-thumb": { - boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", - backgroundColor: "#e8e8e8", - }, + ...theme.scrollbarStyles, }, popoverPaper: { width: "100%", diff --git a/frontend/src/components/PaginationActions/index.js b/frontend/src/components/PaginationActions/index.js deleted file mode 100644 index 854eae7..0000000 --- a/frontend/src/components/PaginationActions/index.js +++ /dev/null @@ -1,71 +0,0 @@ -import React from "react"; - -import { makeStyles } from "@material-ui/core/styles"; - -import FirstPageIcon from "@material-ui/icons/FirstPage"; -import KeyboardArrowLeft from "@material-ui/icons/KeyboardArrowLeft"; -import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight"; -import LastPageIcon from "@material-ui/icons/LastPage"; -import IconButton from "@material-ui/core/IconButton"; - -const useStyles = makeStyles(theme => ({ - root: { - flexShrink: 0, - marginLeft: theme.spacing(2.5), - }, -})); - -const PaginationActions = ({ count, page, rowsPerPage, onChangePage }) => { - const classes = useStyles(); - - const handleFirstPageButtonClick = event => { - onChangePage(event, 0); - }; - - const handleBackButtonClick = event => { - onChangePage(event, page - 1); - }; - - const handleNextButtonClick = event => { - onChangePage(event, page + 1); - }; - - const handleLastPageButtonClick = event => { - onChangePage(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1)); - }; - - return ( -
- - {} - - - {} - - = Math.ceil(count / rowsPerPage) - 1} - aria-label="next page" - > - {} - - = Math.ceil(count / rowsPerPage) - 1} - aria-label="last page" - > - {} - -
- ); -}; - -export default PaginationActions; diff --git a/frontend/src/components/TableRowSkeleton/index.js b/frontend/src/components/TableRowSkeleton/index.js new file mode 100644 index 0000000..e8fc618 --- /dev/null +++ b/frontend/src/components/TableRowSkeleton/index.js @@ -0,0 +1,78 @@ +import React from "react"; +import TableCell from "@material-ui/core/TableCell"; +import TableRow from "@material-ui/core/TableRow"; +import Skeleton from "@material-ui/lab/Skeleton"; + +const TableRowSkeleton = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default TableRowSkeleton; diff --git a/frontend/src/components/TicketsList/index.js b/frontend/src/components/TicketsList/index.js index dabd112..d34b9bd 100644 --- a/frontend/src/components/TicketsList/index.js +++ b/frontend/src/components/TicketsList/index.js @@ -23,14 +23,7 @@ const useStyles = makeStyles(theme => ({ ticketsList: { flex: 1, overflowY: "scroll", - "&::-webkit-scrollbar": { - width: "8px", - height: "8px", - }, - "&::-webkit-scrollbar-thumb": { - boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", - backgroundColor: "#e8e8e8", - }, + ...theme.scrollbarStyles, borderTop: "2px solid rgba(0, 0, 0, 0.12)", }, diff --git a/frontend/src/components/Title/index.js b/frontend/src/components/Title/index.js new file mode 100644 index 0000000..b3fc24c --- /dev/null +++ b/frontend/src/components/Title/index.js @@ -0,0 +1,10 @@ +import React from "react"; +import Typography from "@material-ui/core/Typography"; + +export default function Title(props) { + return ( + + {props.children} + + ); +} diff --git a/frontend/src/components/UserModal/index.js b/frontend/src/components/UserModal/index.js new file mode 100644 index 0000000..f5402bb --- /dev/null +++ b/frontend/src/components/UserModal/index.js @@ -0,0 +1,212 @@ +import React, { useState, useEffect } from "react"; + +import * as Yup from "yup"; +import { Formik, Form, Field } from "formik"; + +import { makeStyles } from "@material-ui/core/styles"; +import { green } from "@material-ui/core/colors"; +import Button from "@material-ui/core/Button"; +import TextField from "@material-ui/core/TextField"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import Select from "@material-ui/core/Select"; +import InputLabel from "@material-ui/core/InputLabel"; +import MenuItem from "@material-ui/core/MenuItem"; +import FormControl from "@material-ui/core/FormControl"; + +// import { i18n } from "../../translate/i18n"; + +import api from "../../services/api"; + +const useStyles = makeStyles(theme => ({ + root: { + display: "flex", + flexWrap: "wrap", + }, + textField: { + // marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + // width: "25ch", + flex: 1, + }, + + btnWrapper: { + // margin: theme.spacing(1), + position: "relative", + }, + + buttonProgress: { + color: green[500], + position: "absolute", + top: "50%", + left: "50%", + marginTop: -12, + marginLeft: -12, + }, + formControl: { + margin: theme.spacing(1), + minWidth: 120, + }, +})); + +const UserSchema = Yup.object().shape({ + name: Yup.string() + .min(2, "Too Short!") + .max(50, "Too Long!") + .required("Required"), + password: Yup.string().min(5, "Too Short!").max(50, "Too Long!"), + email: Yup.string().email("Invalid email").required("Required"), +}); + +const UserModal = ({ open, onClose, userId }) => { + const classes = useStyles(); + + const initialState = { + name: "", + email: "", + password: "", + profile: "user", + }; + + const [user, setUser] = useState(initialState); + + useEffect(() => { + const fetchUser = async () => { + if (!userId) return; + const { data } = await api.get(`/users/${userId}`); + setUser(prevState => { + return { ...prevState, ...data }; + }); + }; + + fetchUser(); + }, [userId, open]); + + const handleClose = () => { + onClose(); + setUser(initialState); + }; + + const handleSaveUser = async values => { + try { + if (userId) { + await api.put(`/users/${userId}`, values); + } else { + await api.post("/users", values); + } + } catch (err) { + alert(JSON.stringify(err.response.data, null, 2)); + console.log(err); + } + handleClose(); + }; + + return ( +
+ + + {userId ? `Edit User` : `New User`} + + { + setTimeout(() => { + handleSaveUser(values); + actions.setSubmitting(false); + }, 400); + }} + > + {({ touched, errors, isSubmitting }) => ( +
+ + + +
+ + + + Profile + + + Admin + User + + +
+
+ + + + +
+ )} +
+
+
+ ); +}; + +export default UserModal; diff --git a/frontend/src/components/_layout/MainListItems.js b/frontend/src/components/_layout/MainListItems.js index abe5727..79ba541 100644 --- a/frontend/src/components/_layout/MainListItems.js +++ b/frontend/src/components/_layout/MainListItems.js @@ -60,12 +60,12 @@ const MainListItems = () => { Administration } /> } /> diff --git a/frontend/src/pages/Contacts/index.js b/frontend/src/pages/Contacts/index.js index 74b882b..e8a7b08 100644 --- a/frontend/src/pages/Contacts/index.js +++ b/frontend/src/pages/Contacts/index.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useReducer } from "react"; import openSocket from "socket.io-client"; import { makeStyles } from "@material-ui/core/styles"; @@ -10,93 +10,100 @@ import TableRow from "@material-ui/core/TableRow"; import Paper from "@material-ui/core/Paper"; import Button from "@material-ui/core/Button"; import Avatar from "@material-ui/core/Avatar"; -import TableFooter from "@material-ui/core/TableFooter"; -import TablePagination from "@material-ui/core/TablePagination"; + import SearchIcon from "@material-ui/icons/Search"; import TextField from "@material-ui/core/TextField"; -import Container from "@material-ui/core/Container"; import InputAdornment from "@material-ui/core/InputAdornment"; -import Typography from "@material-ui/core/Typography"; import IconButton from "@material-ui/core/IconButton"; import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"; import EditIcon from "@material-ui/icons/Edit"; -import PaginationActions from "../../components/PaginationActions"; import api from "../../services/api"; -import ContactsSekeleton from "../../components/ContactsSekeleton"; +import TableRowSkeleton from "../../components/TableRowSkeleton"; import ContactModal from "../../components/ContactModal"; import ConfirmationModal from "../../components/ConfirmationModal/"; import { i18n } from "../../translate/i18n"; +import MainHeader from "../../components/MainHeader"; +import Title from "../../components/Title"; +import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper"; +import MainContainer from "../../components/MainContainer"; + +const reducer = (state, action) => { + if (action.type === "LOAD_CONTACTS") { + const contacts = action.payload; + const newContacts = []; + + contacts.forEach(contact => { + const contactIndex = state.findIndex(c => c.id === contact.id); + if (contactIndex !== -1) { + state[contactIndex] = contact; + } else { + newContacts.push(contact); + } + }); + + return [...state, ...newContacts]; + } + + if (action.type === "UPDATE_CONTACT") { + const updatedContact = action.payload; + const contactIndex = state.findIndex(c => c.id === updatedContact.id); + + if (contactIndex !== -1) { + state[contactIndex] = updatedContact; + } + + return [...state]; + } + + if (action.type === "DELETE_CONTACT") { + const contactId = action.payload; + console.log("cai aqui", contactId); + + const contactIndex = state.findIndex(c => c.id === contactId); + if (contactIndex !== -1) { + console.log("cai no if"); + + state.splice(contactIndex, 1); + } + return [...state]; + } +}; const useStyles = makeStyles(theme => ({ - mainContainer: { - flex: 1, - padding: theme.spacing(2), - height: `calc(100% - 48px)`, - }, - - contentWrapper: { - height: "100%", - overflowY: "hidden", - display: "flex", - flexDirection: "column", - }, - - contactsHeader: { - display: "flex", - alignItems: "center", - padding: "0px 6px 6px 6px", - }, - - actionButtons: { - flex: "none", - marginLeft: "auto", - "& > *": { - margin: theme.spacing(1), - }, - }, - mainPaper: { flex: 1, - padding: theme.spacing(2), + padding: theme.spacing(1), overflowY: "scroll", - "&::-webkit-scrollbar": { - width: "8px", - height: "8px", - }, - "&::-webkit-scrollbar-thumb": { - boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", - backgroundColor: "#e8e8e8", - }, + ...theme.scrollbarStyles, }, })); const Contacts = () => { const classes = useStyles(); - const [loading, setLoading] = useState(true); - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(10); - const [count, setCount] = useState(0); + const [loading, setLoading] = useState(false); + const [pageNumber, setPageNumber] = useState(1); const [searchParam, setSearchParam] = useState(""); - const [contacts, setContacts] = useState([]); + const [contacts, dispatch] = useReducer(reducer, []); const [selectedContactId, setSelectedContactId] = useState(null); const [contactModalOpen, setContactModalOpen] = useState(false); const [deletingContact, setDeletingContact] = useState(null); const [confirmOpen, setConfirmOpen] = useState(false); + const [hasMore, setHasMore] = useState(false); useEffect(() => { setLoading(true); const delayDebounceFn = setTimeout(() => { const fetchContacts = async () => { try { - const res = await api.get("/contacts/", { - params: { searchParam, pageNumber: page + 1, rowsPerPage }, + const { data } = await api.get("/contacts/", { + params: { searchParam, pageNumber }, }); - setContacts(res.data.contacts); - setCount(res.data.count); + dispatch({ type: "LOAD_CONTACTS", payload: data.contacts }); + setHasMore(data.hasMore); setLoading(false); } catch (err) { console.log(err); @@ -106,17 +113,17 @@ const Contacts = () => { fetchContacts(); }, 500); return () => clearTimeout(delayDebounceFn); - }, [searchParam, page, rowsPerPage]); + }, [searchParam, pageNumber]); useEffect(() => { const socket = openSocket(process.env.REACT_APP_BACKEND_URL); socket.on("contact", data => { if (data.action === "update" || data.action === "create") { - updateContacts(data.contact); + dispatch({ type: "UPDATE_CONTACT", payload: data.contact }); } if (data.action === "delete") { - deleteContact(data.contactId); + dispatch({ type: "DELETE_CONTACT", payload: +data.contactId }); } }); @@ -125,40 +132,6 @@ const Contacts = () => { }; }, []); - const updateContacts = contact => { - setContacts(prevState => { - const contactIndex = prevState.findIndex(c => c.id === contact.id); - - if (contactIndex === -1) { - return [contact, ...prevState]; - } - const aux = [...prevState]; - aux[contactIndex] = contact; - return aux; - }); - }; - - const deleteContact = contactId => { - setContacts(prevState => { - const contactIndex = prevState.findIndex(c => c.id === +contactId); - - if (contactIndex === -1) return prevState; - - const aux = [...prevState]; - aux.splice(contactIndex, 1); - return aux; - }); - }; - - const handleChangePage = (event, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = event => { - setRowsPerPage(+event.target.value); - setPage(0); - }; - const handleSearch = event => { setSearchParam(event.target.value.toLowerCase()); }; @@ -186,7 +159,7 @@ const Contacts = () => { } setDeletingContact(null); setSearchParam(""); - setPage(0); + setPageNumber(1); }; const handleimportContact = async () => { @@ -197,8 +170,20 @@ const Contacts = () => { } }; + const loadMore = () => { + setPageNumber(prevState => prevState + 1); + }; + + const handleScroll = e => { + if (!hasMore || loading) return; + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; + if (scrollHeight - (scrollTop + 100) < clientHeight) { + loadMore(); + } + }; + return ( - + { ? `${i18n.t("contacts.confirmationModal.deleteMessage")}` : `${i18n.t("contacts.confirmationModal.importMessage")}`} -
-
- - {i18n.t("contacts.title")} - + + {i18n.t("contacts.title")} + + + + + ), + }} + /> + + + + + + + + + + {i18n.t("contacts.table.name")} + {i18n.t("contacts.table.whatsapp")} + {i18n.t("contacts.table.email")} + + {i18n.t("contacts.table.actions")} + + + + + <> + {contacts.map(contact => ( + + + {} + + {contact.name} + {contact.number} + {contact.email} + + hadleEditContact(contact.id)} + > + + -
- - - - ), - }} - /> - - -
- - -
- - - - {i18n.t("contacts.table.name")} - {i18n.t("contacts.table.whatsapp")} - {i18n.t("contacts.table.email")} - - {i18n.t("contacts.table.actions")} - - - - - {loading ? ( - - ) : ( - <> - {contacts.map(contact => ( - - - {} - - {contact.name} - {contact.number} - {contact.email} - - hadleEditContact(contact.id)} - > - - - - { - setConfirmOpen(true); - setDeletingContact(contact); - }} - > - - - - - ))} - - )} - - - - - - -
-
-
- + { + setConfirmOpen(true); + setDeletingContact(contact); + }} + > + + + + + ))} + {loading && } + + + + + ); }; diff --git a/frontend/src/pages/Dashboard/Title.js b/frontend/src/pages/Dashboard/Title.js index cd59203..8fa5dad 100644 --- a/frontend/src/pages/Dashboard/Title.js +++ b/frontend/src/pages/Dashboard/Title.js @@ -1,10 +1,12 @@ import React from "react"; import Typography from "@material-ui/core/Typography"; -export default function Title(props) { +const Title = props => { return ( {props.children} ); -} +}; + +export default Title; diff --git a/frontend/src/pages/Signup/index.js b/frontend/src/pages/Signup/index.js index c02e995..9823670 100644 --- a/frontend/src/pages/Signup/index.js +++ b/frontend/src/pages/Signup/index.js @@ -1,8 +1,10 @@ import React, { useState } from "react"; +import * as Yup from "yup"; import { useHistory } from "react-router-dom"; import { Link as RouterLink } from "react-router-dom"; import { toast } from "react-toastify"; +import { Formik, Form, Field } from "formik"; import Avatar from "@material-ui/core/Avatar"; import Button from "@material-ui/core/Button"; @@ -53,20 +55,26 @@ const useStyles = makeStyles(theme => ({ }, })); +const UserSchema = Yup.object().shape({ + name: Yup.string() + .min(2, "Too Short!") + .max(50, "Too Long!") + .required("Required"), + password: Yup.string().min(5, "Too Short!").max(50, "Too Long!"), + email: Yup.string().email("Invalid email").required("Required"), +}); + const SignUp = () => { const classes = useStyles(); const history = useHistory(); - const [user, setUser] = useState({ name: "", email: "", password: "" }); + const initialState = { name: "", email: "", password: "" }; - const handleChangeInput = e => { - setUser({ ...user, [e.target.name]: e.target.value }); - }; + const [user] = useState(initialState); - const handleSignUp = async e => { - e.preventDefault(); + const handleSignUp = async values => { try { - await api.post("/users", user); + await api.post("/auth/signup", values); toast.success(i18n.t("signup.toasts.success")); history.push("/login"); } catch (err) { @@ -84,68 +92,88 @@ const SignUp = () => { {i18n.t("signup.title")} -
- - - - + {/* */} + { + setTimeout(() => { + handleSignUp(values); + actions.setSubmitting(false); + }, 400); + }} + > + {({ touched, errors, isSubmitting }) => ( + + + + + - - + + + + + + + - - - - {i18n.t("signup.buttons.login")} - - - - + variant="contained" + color="primary" + className={classes.submit} + > + {i18n.t("signup.buttons.submit")} + + + + + {i18n.t("signup.buttons.login")} + + + + + )} +
diff --git a/frontend/src/pages/Users/index.js b/frontend/src/pages/Users/index.js new file mode 100644 index 0000000..faa76c9 --- /dev/null +++ b/frontend/src/pages/Users/index.js @@ -0,0 +1,234 @@ +import React, { useState, useEffect } from "react"; + +import openSocket from "socket.io-client"; + +import { makeStyles } from "@material-ui/core/styles"; +import Paper from "@material-ui/core/Paper"; +import Button from "@material-ui/core/Button"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import IconButton from "@material-ui/core/IconButton"; +import SearchIcon from "@material-ui/icons/Search"; +import TextField from "@material-ui/core/TextField"; +import InputAdornment from "@material-ui/core/InputAdornment"; + +import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"; +import EditIcon from "@material-ui/icons/Edit"; + +import MainContainer from "../../components/MainContainer"; +import MainHeader from "../../components/MainHeader"; +import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper"; +import Title from "../../components/Title"; + +import api from "../../services/api"; +import TableRowSkeleton from "../../components/TableRowSkeleton"; +import UserModal from "../../components/UserModal"; +import ConfirmationModal from "../../components/ConfirmationModal"; + +const useStyles = makeStyles(theme => ({ + mainPaper: { + flex: 1, + padding: theme.spacing(1), + overflowY: "scroll", + ...theme.scrollbarStyles, + }, +})); + +const Users = () => { + const classes = useStyles(); + + const [loading, setLoading] = useState(false); + const [pageNumber, setPageNumber] = useState(1); + const [selectedUserId, setSelectedUserId] = useState(null); + const [userModalOpen, setUserModalOpen] = useState(false); + const [confirmModalOpen, setConfirmModalOpen] = useState(false); + const [deletingUser, setDeletingUser] = useState(null); + const [searchParam, setSearchParam] = useState(""); + const [users, setUsers] = useState([]); + + useEffect(() => { + setLoading(true); + const delayDebounceFn = setTimeout(() => { + const fetchUsers = async () => { + try { + const res = await api.get("/users/", { + params: { searchParam, pageNumber }, + }); + setUsers(res.data.users); + setLoading(false); + } catch (err) { + console.log(err); + alert(err); + } + }; + fetchUsers(); + }, 500); + return () => clearTimeout(delayDebounceFn); + }, [searchParam, pageNumber]); + + useEffect(() => { + const socket = openSocket(process.env.REACT_APP_BACKEND_URL); + socket.on("user", data => { + if (data.action === "update" || data.action === "create") { + updateUsers(data.user); + } + + if (data.action === "delete") { + deleteUser(data.userId); + } + }); + + return () => { + socket.disconnect(); + }; + }, []); + + const updateUsers = user => { + setUsers(prevState => { + const userIndex = prevState.findIndex(c => c.id === user.id); + + if (userIndex === -1) { + return [user, ...prevState]; + } + const aux = [...prevState]; + aux[userIndex] = user; + return aux; + }); + }; + + const deleteUser = userId => { + setUsers(prevState => { + const userIndex = prevState.findIndex(c => c.id === +userId); + + if (userIndex === -1) return prevState; + + const aux = [...prevState]; + aux.splice(userIndex, 1); + return aux; + }); + }; + + const handleOpenUserModal = () => { + setSelectedUserId(null); + setUserModalOpen(true); + }; + + const handleCloseUserModal = () => { + setSelectedUserId(null); + setUserModalOpen(false); + }; + + const handleSearch = event => { + setSearchParam(event.target.value.toLowerCase()); + }; + + const handleEditUser = userId => { + setSelectedUserId(userId); + setUserModalOpen(true); + }; + + const handleDeleteUser = async userId => { + try { + await api.delete(`/users/${userId}`); + } catch (err) { + alert(err); + } + setDeletingUser(null); + setSearchParam(""); + setPageNumber(1); + }; + + return ( + + handleDeleteUser(deletingUser.id)} + > + Are you sure? It canoot be reverted. + + + + Usuários + + + + + ), + }} + /> + + + + + + + + Name + Email + Profile + Actions + + + + {loading ? ( + + ) : ( + <> + {users.map(user => ( + + {user.name} + {user.email} + {user.profile} + + handleEditUser(user.id)} + > + + + + { + setConfirmModalOpen(true); + setDeletingUser(user); + }} + > + + + + + ))} + + )} + +
+
+
+ ); +}; + +export default Users; diff --git a/frontend/src/routes/index.js b/frontend/src/routes/index.js index bd55707..4042e26 100644 --- a/frontend/src/routes/index.js +++ b/frontend/src/routes/index.js @@ -8,6 +8,7 @@ import Chat from "../pages/Chat/"; import Signup from "../pages/Signup/"; import Login from "../pages/Login/"; import WhatsAuth from "../pages/WhatsAuth/WhatsAuth"; +import Users from "../pages/Users"; import Contacts from "../pages/Contacts/"; import { AuthProvider } from "../context/Auth/AuthContext"; import Route from "./Route"; @@ -24,6 +25,7 @@ const Routes = () => { +