├── frontend
├── .gitignore
├── screenshots
│ ├── MAT.png
│ ├── LOGIN.png
│ ├── ALUNOS.png
│ ├── MAT CAD.png
│ ├── PLANOS.png
│ ├── ALUNO CAD.png
│ ├── HELPORDER.png
│ └── help order msg.png
├── src
│ ├── assets
│ │ ├── icon.png
│ │ ├── img.png
│ │ ├── logo.png
│ │ ├── banner.png
│ │ ├── bgauth.jpg
│ │ ├── logoauth.png
│ │ ├── background.jpg
│ │ ├── background.png
│ │ └── searchicon.png
│ ├── services
│ │ ├── history.js
│ │ └── api.js
│ ├── index.js
│ ├── store
│ │ ├── modules
│ │ │ ├── rootReducer.js
│ │ │ ├── order
│ │ │ │ ├── actions.js
│ │ │ │ └── sagas.js
│ │ │ ├── registration
│ │ │ │ ├── actions.js
│ │ │ │ ├── reducer.js
│ │ │ │ └── sagas.js
│ │ │ ├── rootSaga.js
│ │ │ ├── student
│ │ │ │ ├── reducer.js
│ │ │ │ ├── actions.js
│ │ │ │ └── sagas.js
│ │ │ ├── plan
│ │ │ │ ├── actions.js
│ │ │ │ └── sagas.js
│ │ │ └── auth
│ │ │ │ ├── actions.js
│ │ │ │ ├── reducer.js
│ │ │ │ └── sagas.js
│ │ ├── persistReduce.js
│ │ ├── createStore.js
│ │ └── index.js
│ ├── Pages
│ │ ├── _layouts
│ │ │ ├── default
│ │ │ │ ├── styles.js
│ │ │ │ └── index.js
│ │ │ └── auth
│ │ │ │ ├── index.js
│ │ │ │ └── styles.js
│ │ ├── SignIn
│ │ │ └── index.js
│ │ ├── HelpOrders
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── Plans
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── Registrations
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ └── Students
│ │ │ ├── styles.js
│ │ │ └── index.js
│ ├── components
│ │ ├── Footer
│ │ │ ├── index.js
│ │ │ └── styles.js
│ │ ├── Modals
│ │ │ ├── Order
│ │ │ │ ├── Delete
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── styles.js
│ │ │ │ └── Register
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── styles.js
│ │ │ ├── Registration
│ │ │ │ ├── Delete
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── styles.js
│ │ │ │ └── Register
│ │ │ │ │ ├── styles.js
│ │ │ │ │ └── index.js
│ │ │ ├── Plan
│ │ │ │ ├── Delete
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── styles.js
│ │ │ │ ├── Register
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── styles.js
│ │ │ │ └── Update
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── styles.js
│ │ │ └── Student
│ │ │ │ ├── Delete
│ │ │ │ ├── index.js
│ │ │ │ └── styles.js
│ │ │ │ ├── Update
│ │ │ │ ├── styles.js
│ │ │ │ └── index.js
│ │ │ │ └── Register
│ │ │ │ ├── styles.js
│ │ │ │ └── index.js
│ │ └── Header
│ │ │ ├── index.js
│ │ │ └── styles.js
│ ├── config
│ │ └── ReactotronConfig.js
│ ├── util
│ │ └── util.js
│ ├── Routes
│ │ ├── index.js
│ │ └── route.js
│ ├── styles
│ │ └── global.js
│ └── App.js
├── .prettierrc
├── .editorconfig
├── public
│ └── index.html
├── .eslintrc.js
└── package.json
├── backend
├── .gitignore
├── src
│ ├── app
│ │ ├── views
│ │ │ └── emails
│ │ │ │ ├── answer.hbs
│ │ │ │ ├── partials
│ │ │ │ └── footer.hbs
│ │ │ │ ├── welcome.hbs
│ │ │ │ └── layouts
│ │ │ │ └── default.hbs
│ │ ├── models
│ │ │ ├── Checkin.js
│ │ │ ├── Plan.js
│ │ │ ├── Student.js
│ │ │ ├── Order.js
│ │ │ ├── User.js
│ │ │ └── Registration.js
│ │ ├── jobs
│ │ │ ├── AnswerMail.js
│ │ │ └── RegistrationMail.js
│ │ ├── Controllers
│ │ │ ├── SessionController.js
│ │ │ ├── StudentOrderController.js
│ │ │ ├── UserController.js
│ │ │ ├── CheckinController.js
│ │ │ ├── GymOrderController.js
│ │ │ ├── PlanController.js
│ │ │ ├── StudentController.js
│ │ │ └── RegistrationController.js
│ │ └── middlewares
│ │ │ └── auth.js
│ ├── server.js
│ ├── queue.js
│ ├── tmp
│ │ ├── util
│ │ │ └── logo.png
│ │ └── uploads
│ │ │ └── 38c4274d9d8a1e72.jfif
│ ├── config
│ │ ├── auth.js
│ │ ├── redis.js
│ │ ├── mail.js
│ │ └── database.js
│ ├── database
│ │ ├── migrations
│ │ │ ├── 20200502210540-deleted-column.js
│ │ │ ├── 20200506191453-add-file-column.js
│ │ │ ├── 20200506191150-files.js
│ │ │ ├── 20191214164118-checkins.js
│ │ │ ├── __20200226223724-students.js
│ │ │ ├── __20191212015127-plans.js
│ │ │ ├── 20200217004451-create-user.js
│ │ │ ├── 20191214182657-helporders.js
│ │ │ └── 20191212232840-registrations.js
│ │ ├── seeds
│ │ │ └── 20200228011713-create-plan.js
│ │ └── index.js
│ ├── lib
│ │ ├── Queue.js
│ │ └── Mail.js
│ ├── app.js
│ └── routes.js
├── nodemon.json
├── .prettierrc
├── tmp
│ └── uploads
│ │ ├── 02ce80b6e0c501f7fce3.jpg
│ │ ├── 07ef7e3ee04e066de24e.jpg
│ │ ├── 1382d1b6782f3a8edfbf.jpg
│ │ ├── 7d9d7551e5f7e37564dd.jpg
│ │ ├── abe1f3e999f0bc71f421.jpg
│ │ ├── 5c621684aae4e788d0586617305d2e72.jpg
│ │ ├── 7d21ed04c7cf796ffbebe1e48a9d318d.jpg
│ │ ├── 8370ad7a3a1c1906fd251ad5666a0dbd.jpg
│ │ ├── 9f8cb2523aa18d2dae5e7a61b6ee802c.jpg
│ │ ├── b9f663761a12dd82d3787efc5249992a.jpg
│ │ ├── c8b58c7670be8bb3a4bfa000a1104aa0.jpg
│ │ ├── d8686cd23fcdf13e55664812a012197d.jpg
│ │ └── f97dc2889d4d0e7bec4fc0538215870b.jpg
├── .sequelizerc
├── .eslintrc.js
└── package.json
├── screenshots
├── MAT.png
├── ALUNOS.png
├── LOGIN.png
├── MAT CAD.png
├── PLANOS.png
├── ALUNO CAD.png
├── HELPORDER.png
└── help order msg.png
└── README.md
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | .editorconfig
4 |
5 |
--------------------------------------------------------------------------------
/backend/src/app/views/emails/answer.hbs:
--------------------------------------------------------------------------------
1 |
2 |
{{answer}}
3 |
--------------------------------------------------------------------------------
/backend/src/app/views/emails/partials/footer.hbs:
--------------------------------------------------------------------------------
1 |
2 | Wolfzz
3 |
--------------------------------------------------------------------------------
/backend/src/server.js:
--------------------------------------------------------------------------------
1 | import app from './app';
2 |
3 | app.listen(3333);
4 |
--------------------------------------------------------------------------------
/backend/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "execMap": {
3 | "js": "sucrase-node"
4 | }
5 | }
--------------------------------------------------------------------------------
/backend/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5"
4 | }
5 |
--------------------------------------------------------------------------------
/backend/src/queue.js:
--------------------------------------------------------------------------------
1 | import Queue from './lib/Queue';
2 |
3 | Queue.processQueue();
4 |
--------------------------------------------------------------------------------
/screenshots/MAT.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/screenshots/MAT.png
--------------------------------------------------------------------------------
/screenshots/ALUNOS.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/screenshots/ALUNOS.png
--------------------------------------------------------------------------------
/screenshots/LOGIN.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/screenshots/LOGIN.png
--------------------------------------------------------------------------------
/screenshots/MAT CAD.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/screenshots/MAT CAD.png
--------------------------------------------------------------------------------
/screenshots/PLANOS.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/screenshots/PLANOS.png
--------------------------------------------------------------------------------
/screenshots/ALUNO CAD.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/screenshots/ALUNO CAD.png
--------------------------------------------------------------------------------
/screenshots/HELPORDER.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/screenshots/HELPORDER.png
--------------------------------------------------------------------------------
/frontend/screenshots/MAT.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/frontend/screenshots/MAT.png
--------------------------------------------------------------------------------
/frontend/src/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/frontend/src/assets/icon.png
--------------------------------------------------------------------------------
/frontend/src/assets/img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/frontend/src/assets/img.png
--------------------------------------------------------------------------------
/frontend/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/frontend/src/assets/logo.png
--------------------------------------------------------------------------------
/backend/src/tmp/util/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/backend/src/tmp/util/logo.png
--------------------------------------------------------------------------------
/frontend/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false,
4 | "trailingComma": "es5",
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/screenshots/LOGIN.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/frontend/screenshots/LOGIN.png
--------------------------------------------------------------------------------
/frontend/src/assets/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/frontend/src/assets/banner.png
--------------------------------------------------------------------------------
/frontend/src/assets/bgauth.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/frontend/src/assets/bgauth.jpg
--------------------------------------------------------------------------------
/screenshots/help order msg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/screenshots/help order msg.png
--------------------------------------------------------------------------------
/frontend/screenshots/ALUNOS.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/frontend/screenshots/ALUNOS.png
--------------------------------------------------------------------------------
/frontend/screenshots/MAT CAD.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/frontend/screenshots/MAT CAD.png
--------------------------------------------------------------------------------
/frontend/screenshots/PLANOS.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/frontend/screenshots/PLANOS.png
--------------------------------------------------------------------------------
/frontend/src/assets/logoauth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/frontend/src/assets/logoauth.png
--------------------------------------------------------------------------------
/frontend/screenshots/ALUNO CAD.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/frontend/screenshots/ALUNO CAD.png
--------------------------------------------------------------------------------
/frontend/screenshots/HELPORDER.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/frontend/screenshots/HELPORDER.png
--------------------------------------------------------------------------------
/frontend/src/assets/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/frontend/src/assets/background.jpg
--------------------------------------------------------------------------------
/frontend/src/assets/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/frontend/src/assets/background.png
--------------------------------------------------------------------------------
/frontend/src/assets/searchicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/frontend/src/assets/searchicon.png
--------------------------------------------------------------------------------
/frontend/screenshots/help order msg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/frontend/screenshots/help order msg.png
--------------------------------------------------------------------------------
/backend/src/tmp/uploads/38c4274d9d8a1e72.jfif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/backend/src/tmp/uploads/38c4274d9d8a1e72.jfif
--------------------------------------------------------------------------------
/backend/tmp/uploads/02ce80b6e0c501f7fce3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/backend/tmp/uploads/02ce80b6e0c501f7fce3.jpg
--------------------------------------------------------------------------------
/backend/tmp/uploads/07ef7e3ee04e066de24e.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/backend/tmp/uploads/07ef7e3ee04e066de24e.jpg
--------------------------------------------------------------------------------
/backend/tmp/uploads/1382d1b6782f3a8edfbf.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/backend/tmp/uploads/1382d1b6782f3a8edfbf.jpg
--------------------------------------------------------------------------------
/backend/tmp/uploads/7d9d7551e5f7e37564dd.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/backend/tmp/uploads/7d9d7551e5f7e37564dd.jpg
--------------------------------------------------------------------------------
/backend/tmp/uploads/abe1f3e999f0bc71f421.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/backend/tmp/uploads/abe1f3e999f0bc71f421.jpg
--------------------------------------------------------------------------------
/backend/src/config/auth.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 |
3 | export default {
4 | secret: process.env.AUTH_SECRET,
5 | expiresIn: '7d',
6 | };
7 |
--------------------------------------------------------------------------------
/backend/src/config/redis.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 |
3 | export default {
4 | host: process.env.REDIS_HOST,
5 | port: process.env.REDIS_PORT,
6 | };
7 |
--------------------------------------------------------------------------------
/frontend/src/services/history.js:
--------------------------------------------------------------------------------
1 | import { createBrowserHistory } from 'history'
2 |
3 | const history = createBrowserHistory()
4 |
5 | export default history
6 |
--------------------------------------------------------------------------------
/backend/tmp/uploads/5c621684aae4e788d0586617305d2e72.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/backend/tmp/uploads/5c621684aae4e788d0586617305d2e72.jpg
--------------------------------------------------------------------------------
/backend/tmp/uploads/7d21ed04c7cf796ffbebe1e48a9d318d.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/backend/tmp/uploads/7d21ed04c7cf796ffbebe1e48a9d318d.jpg
--------------------------------------------------------------------------------
/backend/tmp/uploads/8370ad7a3a1c1906fd251ad5666a0dbd.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/backend/tmp/uploads/8370ad7a3a1c1906fd251ad5666a0dbd.jpg
--------------------------------------------------------------------------------
/backend/tmp/uploads/9f8cb2523aa18d2dae5e7a61b6ee802c.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/backend/tmp/uploads/9f8cb2523aa18d2dae5e7a61b6ee802c.jpg
--------------------------------------------------------------------------------
/backend/tmp/uploads/b9f663761a12dd82d3787efc5249992a.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/backend/tmp/uploads/b9f663761a12dd82d3787efc5249992a.jpg
--------------------------------------------------------------------------------
/backend/tmp/uploads/c8b58c7670be8bb3a4bfa000a1104aa0.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/backend/tmp/uploads/c8b58c7670be8bb3a4bfa000a1104aa0.jpg
--------------------------------------------------------------------------------
/backend/tmp/uploads/d8686cd23fcdf13e55664812a012197d.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/backend/tmp/uploads/d8686cd23fcdf13e55664812a012197d.jpg
--------------------------------------------------------------------------------
/backend/tmp/uploads/f97dc2889d4d0e7bec4fc0538215870b.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IgorCruzz/WOLFZZ/HEAD/backend/tmp/uploads/f97dc2889d4d0e7bec4fc0538215870b.jpg
--------------------------------------------------------------------------------
/frontend/src/services/api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const api = axios.create({
4 | baseURL: 'http://localhost:3333',
5 | })
6 |
7 | export default api
8 |
--------------------------------------------------------------------------------
/frontend/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 |
5 | ReactDOM.render(, document.getElementById('root'))
6 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/rootReducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 |
3 | import auth from './auth/reducer'
4 | import student from './student/reducer'
5 | import registration from './registration/reducer'
6 |
7 | export default combineReducers({ auth, student, registration })
8 |
--------------------------------------------------------------------------------
/frontend/src/Pages/_layouts/default/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | import background from '../../../assets/background.png'
4 |
5 | export const Wrapper = styled.div`
6 | background: url(${background}) no-repeat;
7 | background-size: cover;
8 | height: 100%;
9 | width: 100%;
10 | `
11 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/order/actions.js:
--------------------------------------------------------------------------------
1 | export function orderAnswer(data) {
2 | return {
3 | type: '@order/SEND_REPLY',
4 | payload: { data },
5 | }
6 | }
7 |
8 | export const orderDelete = ({ id }) => {
9 | return {
10 | type: '@order/ORDER_DELETE',
11 | payload: { id },
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/backend/src/config/mail.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 |
3 | export default {
4 | host: process.env.MAIL_HOST,
5 | port: process.env.MAIL_PORT,
6 | secure: false,
7 | auth: { user: process.env.MAIL_USER, pass: process.env.MAIL_PASS },
8 | default: {
9 | from: 'Equipe wolfzz {
3 | return queryInterface.addColumn('plans', 'deleted', {
4 | type: Sequelize.BOOLEAN,
5 | allowNull: false,
6 | defaultValue: false,
7 | });
8 | },
9 |
10 | down: () => {},
11 | };
12 |
--------------------------------------------------------------------------------
/frontend/src/components/Footer/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Container } from './styles'
3 |
4 | import logo from '../../assets/logo.png'
5 |
6 | export default function Footer() {
7 | return (
8 |
9 |
10 |

11 |
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/backend/.sequelizerc:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path')
2 |
3 |
4 | module.exports = {
5 | config: resolve(__dirname, 'src', 'config', 'database.js'),
6 | 'models-path': resolve(__dirname, 'src', 'app', 'models'),
7 | 'migrations-path': resolve(__dirname, 'src', 'database', 'migrations'),
8 | 'seeders-path': resolve(__dirname, 'src', 'database', 'seeds')
9 | }
10 |
--------------------------------------------------------------------------------
/backend/src/config/database.js:
--------------------------------------------------------------------------------
1 | require('dotenv/config');
2 |
3 | module.exports = {
4 | dialect: 'postgres',
5 | host: process.env.DB_HOST,
6 | username: process.env.DB_USER,
7 | password: process.env.DB_PASS,
8 | database: process.env.DB_NAME,
9 | define: {
10 | timestamps: true,
11 | underscored: true,
12 | underscoredAll: true,
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/registration/actions.js:
--------------------------------------------------------------------------------
1 | export const RegisterStore = ({ student_id, plan_id }) => {
2 | return {
3 | type: '@registration/STORE_REGISTER',
4 | payload: { student_id, plan_id },
5 | }
6 | }
7 |
8 | export const DeleteRegistration = ({ id }) => {
9 | return {
10 | type: '@registration/DELETE_REGISTER',
11 | payload: { id },
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/backend/src/app/views/emails/welcome.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 | Olá {{ student }}, sejá bem vindo
4 |
5 | Dados da Matricula
6 | Plano: {{ plan }}
7 | Preço: R$ {{ price }}
8 | Validade até: {{ validate }}
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/frontend/src/store/persistReduce.js:
--------------------------------------------------------------------------------
1 | import storage from 'redux-persist/lib/storage'
2 | import { persistReducer } from 'redux-persist'
3 |
4 | export default reducers => {
5 | const persistedReducer = persistReducer(
6 | {
7 | key: 'wolfzzgym',
8 | storage,
9 | whitelist: ['auth'],
10 | },
11 | reducers
12 | )
13 |
14 | return persistedReducer
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/src/store/createStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, compose, applyMiddleware } from 'redux'
2 |
3 | export default (reducers, middlewares) => {
4 | const enhancer =
5 | process.env.NODE_ENV === 'development'
6 | ? compose(console.tron.createEnhancer(), applyMiddleware(...middlewares))
7 | : applyMiddleware(...middlewares)
8 | return createStore(reducers, enhancer)
9 | }
10 |
--------------------------------------------------------------------------------
/backend/src/database/migrations/20200506191453-add-file-column.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: (queryInterface, Sequelize) => {
5 | return queryInterface.addColumn('students', 'file',
6 | {
7 | type: Sequelize.INTEGER,
8 | references: { model: 'files', key: 'id'},
9 | onDelete: 'CASCADE'
10 | });
11 | },
12 |
13 | down: () => {}
14 | };
15 |
--------------------------------------------------------------------------------
/frontend/src/Pages/_layouts/auth/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Wrapper, Content } from './styles'
4 |
5 | export default function AuthLayout({ children }) {
6 | return (
7 |
8 | {children}
9 |
10 | )
11 | }
12 |
13 | AuthLayout.propTypes = {
14 | children: PropTypes.element.isRequired,
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/rootSaga.js:
--------------------------------------------------------------------------------
1 | import { all } from 'redux-saga/effects'
2 |
3 | import auth from './auth/sagas'
4 | import student from './student/sagas'
5 | import plan from './plan/sagas'
6 | import order from './order/sagas'
7 | import registration from './registration/sagas'
8 |
9 | export default function* rootSaga() {
10 | return yield all([auth, student, plan, order, registration])
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/student/reducer.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer'
2 |
3 | const INITIAL_STATE = {
4 | close: false,
5 | }
6 |
7 | export default function student(state = INITIAL_STATE, action) {
8 | return produce(state, draft => {
9 | switch (action.type) {
10 | case '@student/SIGNUP_SUCCESS': {
11 | draft.close = true
12 | break
13 | }
14 | default:
15 | }
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/backend/src/app/models/Checkin.js:
--------------------------------------------------------------------------------
1 | import { Model } from 'sequelize';
2 |
3 | class Checkin extends Model {
4 | static init(sequelize) {
5 | super.init(
6 | {},
7 | {
8 | sequelize,
9 | }
10 | );
11 | return Checkin;
12 | }
13 |
14 | static associate(models) {
15 | this.belongsTo(models.Student, { foreignKey: 'student_id', as: 'student' });
16 | }
17 | }
18 | export default Checkin;
19 |
--------------------------------------------------------------------------------
/frontend/src/config/ReactotronConfig.js:
--------------------------------------------------------------------------------
1 | import Reactotron from 'reactotron-react-js'
2 | import { reactotronRedux } from 'reactotron-redux'
3 | import reactotronSaga from 'reactotron-redux-saga'
4 |
5 | if (process.env.NODE_ENV === 'development') {
6 | const tron = Reactotron.configure()
7 | .use(reactotronRedux())
8 | .use(reactotronSaga())
9 | .connect()
10 |
11 | tron.clear()
12 |
13 | console.tron = tron
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/util/util.js:
--------------------------------------------------------------------------------
1 | export const PriceFormat = value => {
2 | const Currency = value.toLocaleString('pt-BR', {
3 | style: 'currency',
4 | currency: 'BRL',
5 | })
6 |
7 | return Currency
8 | }
9 |
10 | export const Truncate = (value, size) => {
11 | const truncateValue = value.substr(0, size)
12 |
13 | if (truncateValue.length !== size) {
14 | return truncateValue
15 | }
16 |
17 | return `${truncateValue}...`
18 | }
19 |
--------------------------------------------------------------------------------
/backend/src/app/models/Plan.js:
--------------------------------------------------------------------------------
1 | import Sequelize, { Model } from 'sequelize';
2 |
3 | class Plan extends Model {
4 | static init(sequelize) {
5 | super.init(
6 | {
7 | title: Sequelize.STRING,
8 | price: Sequelize.FLOAT,
9 | duration: Sequelize.INTEGER,
10 | deleted: Sequelize.BOOLEAN,
11 | },
12 | {
13 | sequelize,
14 | }
15 | );
16 | return Plan;
17 | }
18 | }
19 | export default Plan;
20 |
--------------------------------------------------------------------------------
/frontend/src/components/Footer/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Container = styled.footer`
4 | padding: 10px;
5 | height: 70px;
6 | background: rgba(7, 7, 7, 0.35);
7 | bottom: 0;
8 | position: absolute;
9 | width: 100%;
10 | display: flex;
11 | align-items: center;
12 | justify-content: center;
13 | border-top: 1px solid #777;
14 |
15 | div {
16 | img {
17 | height: 25px;
18 | }
19 | }
20 | `
21 |
--------------------------------------------------------------------------------
/backend/src/app/jobs/AnswerMail.js:
--------------------------------------------------------------------------------
1 | import Mail from '../../lib/Mail';
2 |
3 | class AnswerMail {
4 | get key() {
5 | return 'AnswerMail';
6 | }
7 |
8 | async handle({ data }) {
9 | const { answer, email } = data;
10 | Mail.sendMail({
11 | to: email,
12 | subject: 'WOLFZZ GYM - RESPOSTA',
13 | template: 'answer',
14 | context: {
15 | answer,
16 | },
17 | });
18 | }
19 | }
20 | export default new AnswerMail();
21 |
--------------------------------------------------------------------------------
/backend/src/app/models/Student.js:
--------------------------------------------------------------------------------
1 | import Sequelize, { Model } from 'sequelize';
2 |
3 | class Student extends Model {
4 | static init(sequelize) {
5 | super.init(
6 | {
7 | name: Sequelize.STRING,
8 | email: Sequelize.STRING,
9 | age: Sequelize.STRING,
10 | height: Sequelize.STRING,
11 | weight: Sequelize.STRING,
12 | },
13 | {
14 | sequelize,
15 | }
16 | );
17 | return Student;
18 | }
19 | }
20 | export default Student;
21 |
--------------------------------------------------------------------------------
/backend/src/app/Controllers/SessionController.js:
--------------------------------------------------------------------------------
1 | import Student from '../models/Student';
2 |
3 | class SessionController {
4 | async store(req, res) {
5 | const { id } = req.body;
6 |
7 | const student = await Student.findOne({
8 | where: { id },
9 | });
10 |
11 | if (!student) {
12 | return res.status(400).json({ error: 'ID inválido' });
13 | }
14 |
15 | return res.json({
16 | student,
17 | });
18 | }
19 | }
20 |
21 | export default new SessionController();
22 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/registration/reducer.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer'
2 |
3 | const INITIAL_VALUE = {
4 | download: false,
5 | close: false,
6 | }
7 |
8 | export default function registration(state = INITIAL_VALUE, action) {
9 | return produce(state, draft => {
10 | switch (action.type) {
11 | case '@registration/STORE_REGISTER_SUCCESS': {
12 | draft.download = true
13 | draft.close = true
14 | break
15 | }
16 | default:
17 | }
18 | })
19 | }
20 |
--------------------------------------------------------------------------------
/backend/src/app/views/emails/layouts/default.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Wolfzz
6 |
7 |
19 |
20 |
21 | {{{ body }}}
22 | {{> footer}}
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/plan/actions.js:
--------------------------------------------------------------------------------
1 | export function registerPlan({ title, price, duration }) {
2 | return {
3 | type: '@plan/REGISTER_PLAN',
4 | payload: { title, price, duration },
5 | }
6 | }
7 |
8 | export function deletePlan({ id }) {
9 | return {
10 | type: '@plan/DELETE_PLAN',
11 | payload: { id },
12 | }
13 | }
14 |
15 | export function updatePlan({ id, title, price, duration }) {
16 | return {
17 | type: '@plan/UPDATE_PLAN',
18 | payload: { id, title, price, duration },
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/backend/src/app/models/Order.js:
--------------------------------------------------------------------------------
1 | import Sequelize, { Model } from 'sequelize';
2 |
3 | class Order extends Model {
4 | static init(sequelize) {
5 | super.init(
6 | {
7 | question: Sequelize.STRING,
8 | answer: Sequelize.STRING,
9 | answer_at: Sequelize.DATE,
10 | },
11 | { sequelize }
12 | );
13 | return Order;
14 | }
15 |
16 | static associate(models) {
17 | this.belongsTo(models.Student, { foreignKey: 'student_id', as: 'student' });
18 | }
19 | }
20 | export default Order;
21 |
--------------------------------------------------------------------------------
/frontend/src/Pages/_layouts/default/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { Wrapper } from './styles'
5 | import Header from '../../../components/Header'
6 | import Footer from '../../../components/Footer'
7 |
8 | export default function DefaultLayout({ children }) {
9 | return (
10 |
11 |
12 | {children}
13 |
14 |
15 | )
16 | }
17 |
18 | DefaultLayout.propTypes = {
19 | children: PropTypes.element.isRequired,
20 | }
21 |
--------------------------------------------------------------------------------
/backend/src/database/seeds/20200228011713-create-plan.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcryptjs');
2 |
3 | module.exports = {
4 | up: queryInterface => {
5 | return queryInterface.bulkInsert(
6 | 'users',
7 | [
8 | {
9 | name: 'Administrador',
10 | email: 'admin@wolfzz.com',
11 | password_hash: bcrypt.hashSync('12345678', 8),
12 | created_at: new Date(),
13 | updated_at: new Date(),
14 | },
15 | ],
16 | {}
17 | );
18 | },
19 |
20 | down: () => {},
21 | };
22 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/auth/actions.js:
--------------------------------------------------------------------------------
1 | export function signInRequest(email, password) {
2 | return {
3 | type: '@auth/SIGN_IN_REQUEST',
4 | payload: { email, password },
5 | }
6 | }
7 |
8 | export function signInSuccess(token, user) {
9 | return {
10 | type: '@auth/SIGN_IN_SUCCESS',
11 | payload: { token, user },
12 | }
13 | }
14 |
15 | export function signFailure() {
16 | return {
17 | type: '@auth/SIGN_FAILURE',
18 | }
19 | }
20 |
21 | export function signOut() {
22 | return {
23 | type: '@auth/SIGN_OUT',
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/backend/src/database/migrations/20200506191150-files.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | up: (queryInterface, Sequelize) => {
5 | return queryInterface.createTable('files',
6 | {
7 | id:{
8 | type:Sequelize.INTEGER,
9 | autoIncrement: true,
10 | primaryKey: true
11 | },
12 | name: {
13 | type: Sequelize.STRING,
14 | allowNull: false
15 | },
16 | created_at: Sequelize.DATE,
17 | updated_at: Sequelize.DATE
18 | });
19 |
20 | },
21 |
22 | down: () => {
23 |
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/backend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es6: true,
4 | node: true,
5 | },
6 | extends: ['airbnb-base',"prettier"],
7 | plugins: ["prettier"],
8 | globals: {
9 | Atomics: 'readonly',
10 | SharedArrayBuffer: 'readonly',
11 | },
12 | parserOptions: {
13 | ecmaVersion: 2018,
14 | sourceType: 'module',
15 | },
16 | rules: {
17 | "prettier/prettier": "error",
18 | "class-methods-use-this": "off",
19 | "no-param-reassign": "off",
20 | "camelcase": "off",
21 | "no-unused-vars": ["error", {"argsIgnorePattern":"next"}]
22 |
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/backend/src/app/models/User.js:
--------------------------------------------------------------------------------
1 | import Sequelize, { Model } from 'sequelize';
2 | import bcrypt from 'bcryptjs';
3 |
4 | class User extends Model {
5 | static init(sequelize) {
6 | super.init(
7 | {
8 | name: Sequelize.STRING,
9 | email: Sequelize.STRING,
10 | password_hash: Sequelize.STRING,
11 | provider: Sequelize.BOOLEAN,
12 | },
13 | {
14 | sequelize,
15 | }
16 | );
17 |
18 | return User;
19 | }
20 |
21 | checkPassword(password) {
22 | return bcrypt.compare(password, this.password_hash);
23 | }
24 | }
25 |
26 | export default User;
27 |
--------------------------------------------------------------------------------
/backend/src/app/models/Registration.js:
--------------------------------------------------------------------------------
1 | import Sequelize, { Model } from 'sequelize';
2 |
3 | class Registration extends Model {
4 | static init(sequelize) {
5 | super.init(
6 | {
7 | start_date: Sequelize.DATE,
8 | end_date: Sequelize.DATE,
9 | price: Sequelize.FLOAT,
10 | },
11 | {
12 | sequelize,
13 | }
14 | );
15 | return Registration;
16 | }
17 |
18 | static associate(models) {
19 | this.belongsTo(models.Student, { foreignKey: 'student_id', as: 'student' });
20 | this.belongsTo(models.Plan, { foreignKey: 'plan_id', as: 'plan' });
21 | }
22 | }
23 | export default Registration;
24 |
--------------------------------------------------------------------------------
/backend/src/app/middlewares/auth.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 | import { promisify } from 'util';
3 | import authConfig from '../../config/auth';
4 |
5 | export default async (req, res, next) => {
6 | const authHeader = req.headers.authorization;
7 |
8 | if (!authHeader) {
9 | return res.status(401).json({ error: 'token not provided' });
10 | }
11 |
12 | const [, token] = authHeader.split(' ');
13 |
14 | try {
15 | const decoded = await promisify(jwt.verify)(token, authConfig.secret);
16 | req.userId = decoded.id;
17 | next();
18 | } catch (err) {
19 | return res.status(401).json({ error: 'token invalid' });
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
16 |
17 | Wolfzz
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/frontend/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { persistStore } from 'redux-persist'
2 | import createSagaMiddleware from 'redux-saga'
3 | import persistReducer from './persistReduce'
4 |
5 | import createStore from './createStore'
6 |
7 | import rootReducer from './modules/rootReducer'
8 | import rootSaga from './modules/rootSaga'
9 |
10 | const sagaMonitor = console.tron.createSagaMonitor()
11 |
12 | const sagaMiddleware = createSagaMiddleware({ sagaMonitor })
13 |
14 | const middlewares = [sagaMiddleware]
15 |
16 | const store = createStore(persistReducer(rootReducer), middlewares)
17 | const persistor = persistStore(store)
18 |
19 | sagaMiddleware.run(rootSaga)
20 |
21 | export { store, persistor }
22 |
--------------------------------------------------------------------------------
/backend/src/app/jobs/RegistrationMail.js:
--------------------------------------------------------------------------------
1 | import { addDays, format } from 'date-fns';
2 | import Mail from '../../lib/Mail';
3 |
4 | class RegistrationMail {
5 | get key() {
6 | return 'RegistrationMail';
7 | }
8 |
9 | async handle({ data }) {
10 | const { student, plan } = data;
11 |
12 | const lastDay = addDays(new Date(), plan.duration);
13 | Mail.sendMail({
14 | to: student.email,
15 | subject: 'NOVA MATRICULA',
16 | template: 'welcome',
17 | context: {
18 | student: student.name,
19 | plan: plan.title,
20 | price: plan.price,
21 | validate: format(lastDay, 'dd/MM/yyyy'),
22 | },
23 | });
24 | }
25 | }
26 | export default new RegistrationMail();
27 |
--------------------------------------------------------------------------------
/frontend/src/Routes/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Switch } from 'react-router-dom'
3 | import Route from './route'
4 | import SignIn from '../Pages/SignIn'
5 |
6 | import Students from '../Pages/Students'
7 | import HelpOrders from '../Pages/HelpOrders'
8 | import Plans from '../Pages/Plans'
9 | import Registrations from '../Pages/Registrations'
10 |
11 | export default function Routes() {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/src/styles/global.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components'
2 |
3 | import 'react-perfect-scrollbar/dist/css/styles.css'
4 | import 'react-toastify/dist/ReactToastify.css'
5 |
6 | export default createGlobalStyle`
7 | * {
8 | margin: 0;
9 | padding: 0;
10 | outline: 0;
11 | box-sizing: border-box
12 | }
13 |
14 | *:focus{
15 | outline: 0
16 | }
17 |
18 | html, body, #root {
19 | height: 100%;
20 | }
21 |
22 | body {
23 | -webkit-font-smoothing: antialiased;
24 | }
25 |
26 | body, input, button {
27 | font: 14px 'Roboto', sans-serif;
28 | }
29 |
30 |
31 | a {
32 | text-decoration: none;
33 | }
34 |
35 | ul {
36 | list-style: none;
37 | }
38 |
39 | button {
40 | cursor: pointer;
41 | font-weight: bold;
42 | }
43 | `
44 |
--------------------------------------------------------------------------------
/backend/src/database/migrations/20191214164118-checkins.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | up: (queryInterface, Sequelize) => {
3 | return queryInterface.createTable('checkins', {
4 | id: {
5 | type: Sequelize.INTEGER,
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | },
10 | student_id: {
11 | type: Sequelize.INTEGER,
12 | references: { model: 'students', key: 'id' },
13 | allowNull: false,
14 | },
15 | created_at: {
16 | type: Sequelize.DATE,
17 | allowNull: false,
18 | },
19 | updated_at: {
20 | type: Sequelize.DATE,
21 | allowNull: false,
22 | },
23 | });
24 | },
25 |
26 | down: queryInterface => {
27 | return queryInterface.dropTable('checkins');
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/frontend/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ToastContainer } from 'react-toastify'
3 | import { PersistGate } from 'redux-persist/integration/react'
4 | import { Router } from 'react-router-dom'
5 | import { Provider } from 'react-redux'
6 | import './config/ReactotronConfig'
7 | import Routes from './Routes'
8 | import GlobalStyle from './styles/global'
9 | import history from './services/history'
10 | import { store, persistor } from './store'
11 |
12 | function App() {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | )
24 | }
25 |
26 | export default App
27 |
--------------------------------------------------------------------------------
/backend/src/database/index.js:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 |
3 | import User from '../app/models/User';
4 | import Student from '../app/models/Student';
5 | import Plan from '../app/models/Plan';
6 | import Registration from '../app/models/Registration';
7 | import Checkin from '../app/models/Checkin';
8 | import Order from '../app/models/Order';
9 |
10 | import databaseConfig from '../config/database';
11 |
12 | const models = [User, Student, Plan, Registration, Checkin, Order];
13 |
14 | class Database {
15 | constructor() {
16 | this.init();
17 | }
18 |
19 | init() {
20 | this.connection = new Sequelize(databaseConfig);
21 |
22 | models
23 | .map(model => model.init(this.connection))
24 | .map(model => model.associate && model.associate(this.connection.models));
25 | }
26 | }
27 |
28 | export default new Database();
29 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/registration/sagas.js:
--------------------------------------------------------------------------------
1 | import { all, call, takeLatest } from 'redux-saga/effects'
2 | import { toast } from 'react-toastify'
3 |
4 | import api from '../../../services/api'
5 |
6 | export function* RegisterStore({ payload }) {
7 | try {
8 | yield call(api.post, 'registration', { ...payload })
9 | toast.success('Matricula realizada com sucesso')
10 | window.location.reload()
11 | } catch (err) {
12 | toast.error('Preencha o formulário')
13 | }
14 | }
15 |
16 | export function* Registerdelete({ payload }) {
17 | yield call(api.delete, `registration/${payload.id}`)
18 |
19 | toast.success('Matricula removida')
20 | window.location.reload()
21 | }
22 |
23 | export default all([
24 | takeLatest('@registration/STORE_REGISTER', RegisterStore),
25 | takeLatest('@registration/DELETE_REGISTER', Registerdelete),
26 | ])
27 |
--------------------------------------------------------------------------------
/backend/src/app/Controllers/StudentOrderController.js:
--------------------------------------------------------------------------------
1 | import * as Yup from 'yup';
2 | import Order from '../models/Order';
3 |
4 | class StudentOrderController {
5 | async store(req, res) {
6 | const schema = Yup.object().shape({
7 | question: Yup.string().required(),
8 | });
9 |
10 | if (!(await schema.isValid(req.body))) {
11 | return res.status(400).json({ error: 'Erro na validação' });
12 | }
13 | const { question } = req.body;
14 |
15 | const questions = await Order.create({
16 | question,
17 | student_id: req.params.id,
18 | });
19 | return res.json(questions);
20 | }
21 |
22 | async index(req, res) {
23 | const showOrder = await Order.findAll({
24 | where: { student_id: req.params.id },
25 | });
26 | return res.json(showOrder);
27 | }
28 | }
29 | export default new StudentOrderController();
30 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/auth/reducer.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer'
2 |
3 | const INITIAL_STATE = {
4 | token: null,
5 | signed: false,
6 | loading: false,
7 | }
8 |
9 | export default function auth(state = INITIAL_STATE, action) {
10 | return produce(state, draft => {
11 | switch (action.type) {
12 | case '@auth/SIGN_IN_REQUEST': {
13 | draft.loading = true
14 | break
15 | }
16 | case '@auth/SIGN_IN_SUCCESS': {
17 | draft.token = action.payload.token
18 | draft.signed = true
19 | draft.loading = false
20 | break
21 | }
22 | case '@auth/SIGN_FAILURE': {
23 | draft.loading = false
24 | break
25 | }
26 | case '@auth/SIGN_OUT': {
27 | draft.token = null
28 | draft.signed = false
29 | break
30 | }
31 | default:
32 | }
33 | })
34 | }
35 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/order/sagas.js:
--------------------------------------------------------------------------------
1 | import { takeLatest, call, all } from 'redux-saga/effects'
2 |
3 | import { toast } from 'react-toastify'
4 |
5 | import api from '../../../services/api'
6 |
7 | export function* OrderStore({ payload }) {
8 | try {
9 | const { id, answer } = payload.data
10 |
11 | yield call(api.post, `help-orders/${id}/answer`, { answer })
12 |
13 | toast.success('Resposta enviada')
14 | window.location.reload()
15 | } catch (err) {
16 | toast.error('Insira uma mensagem')
17 | }
18 | }
19 |
20 | export function* OrderDelete({ payload }) {
21 | const { id } = payload
22 |
23 | yield call(api.delete, `help-orders/${id}`)
24 |
25 | toast.success('Mensagem removida')
26 | window.location.reload()
27 | }
28 |
29 | export default all([
30 | takeLatest('@order/SEND_REPLY', OrderStore),
31 | takeLatest('@order/ORDER_DELETE', OrderDelete),
32 | ])
33 |
--------------------------------------------------------------------------------
/backend/src/lib/Queue.js:
--------------------------------------------------------------------------------
1 | import Bee from 'bee-queue';
2 | import RegistrationMail from '../app/jobs/RegistrationMail';
3 | import AnswerMail from '../app/jobs/AnswerMail';
4 | import redisConfig from '../config/redis';
5 |
6 | const jobs = [RegistrationMail, AnswerMail];
7 |
8 | class Queue {
9 | constructor() {
10 | this.queues = {};
11 |
12 | this.init();
13 | }
14 |
15 | init() {
16 | jobs.forEach(({ key, handle }) => {
17 | this.queues[key] = {
18 | bee: new Bee(key, {
19 | redis: redisConfig,
20 | }),
21 | handle,
22 | };
23 | });
24 | }
25 |
26 | add(queue, job) {
27 | return this.queues[queue].bee.createJob(job).save();
28 | }
29 |
30 | processQueue() {
31 | jobs.forEach(job => {
32 | const { bee, handle } = this.queues[job.key];
33 |
34 | bee.process(handle);
35 | });
36 | }
37 | }
38 |
39 | export default new Queue();
40 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/student/actions.js:
--------------------------------------------------------------------------------
1 | export function studentSignUp({ name, email, age, weight, height }) {
2 | return {
3 | type: '@student/SIGNUP_STUDENT_REQUEST',
4 | payload: { name, email, age, weight, height },
5 | }
6 | }
7 |
8 | export function deleteStudent({ id }) {
9 | return {
10 | type: '@student/DELETE_STUDENT',
11 | payload: { id },
12 | }
13 | }
14 |
15 | export function updateStudentRequest({ id, name, email, age, weight, height }) {
16 | return {
17 | type: '@student/UPDATE_STUDENT_REQUEST',
18 | payload: { id, name, email, age, weight, height },
19 | }
20 | }
21 |
22 | export function findStudentRequest(name) {
23 | return {
24 | type: '@student/FIND_STUDENT',
25 | payload: { name },
26 | }
27 | }
28 |
29 | export function findStudentSuccess(profile) {
30 | return {
31 | type: '@student/FIND_STUDENT_SUCCESS',
32 | payload: { profile },
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/backend/src/database/migrations/__20200226223724-students.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | up: (queryInterface, Sequelize) => {
3 | return queryInterface.createTable('students', {
4 | id: {
5 | type: Sequelize.INTEGER,
6 | allowNull: false,
7 | primaryKey: true,
8 | autoIncrement: true,
9 | },
10 | name: {
11 | type: Sequelize.STRING,
12 | allowNull: false,
13 | },
14 | email: {
15 | type: Sequelize.STRING,
16 | allowNull: false,
17 | unique: true,
18 | },
19 | age: {
20 | type: Sequelize.INTEGER,
21 | allowNull: false,
22 | },
23 | weight: {
24 | type: Sequelize.STRING,
25 | allowNull: false,
26 | },
27 | height: {
28 | type: Sequelize.STRING,
29 | allowNull: false,
30 | },
31 | created_at: Sequelize.DATE,
32 | updated_at: Sequelize.DATE,
33 | });
34 | },
35 |
36 | down: queryInterface => {
37 | return queryInterface.dropTable('students');
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/backend/src/database/migrations/__20191212015127-plans.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | up: (queryInterface, Sequelize) => {
3 | return queryInterface.createTable('plans', {
4 | id: {
5 | type: Sequelize.INTEGER,
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | },
10 | title: {
11 | type: Sequelize.STRING,
12 | allowNull: false,
13 | },
14 | price: {
15 | type: Sequelize.FLOAT,
16 | allowNull: false,
17 | },
18 | duration: {
19 | type: Sequelize.INTEGER,
20 | allowNull: false,
21 | },
22 | created_at: {
23 | type: Sequelize.DATE,
24 | allowNull: false,
25 | },
26 | deleted: {
27 | type: Sequelize.BOOLEAN,
28 | allowNull: false,
29 | defaultValue: false,
30 | },
31 | updated_at: {
32 | type: Sequelize.DATE,
33 | allowNull: false,
34 | },
35 | });
36 | },
37 |
38 | down: queryInterface => {
39 | return queryInterface.dropTable('plan');
40 | },
41 | };
42 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Order/Delete/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDispatch } from 'react-redux'
3 | import PropTypes from 'prop-types'
4 | import { Container, Content } from './styles'
5 |
6 | import { orderDelete } from '../../../../store/modules/order/actions'
7 |
8 | export default function PlanDelete({ open, close, planId }) {
9 | const dispatch = useDispatch()
10 |
11 | const handleSubmit = () => {
12 | dispatch(orderDelete({ id: planId }))
13 | }
14 |
15 | return (
16 |
17 |
18 | Você tem certeza disso?
19 |
20 |
23 |
26 |
27 |
28 |
29 | )
30 | }
31 | PlanDelete.propTypes = {
32 | open: PropTypes.bool.isRequired,
33 | close: PropTypes.func.isRequired,
34 | planId: PropTypes.arrayOf(PropTypes.number).isRequired,
35 | }
36 |
--------------------------------------------------------------------------------
/backend/src/database/migrations/20200217004451-create-user.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | up: (queryInterface, Sequelize) => {
3 | return queryInterface.createTable('users', {
4 | id: {
5 | type: Sequelize.INTEGER,
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | },
10 | name: {
11 | type: Sequelize.STRING,
12 | allowNull: false,
13 | },
14 | email: {
15 | type: Sequelize.STRING,
16 | allowNull: false,
17 | unique: true,
18 | },
19 | password_hash: {
20 | type: Sequelize.STRING,
21 | allowNull: false,
22 | },
23 | provider: {
24 | type: Sequelize.BOOLEAN,
25 | defaultValue: false,
26 | allowNull: false,
27 | },
28 | created_at: {
29 | type: Sequelize.DATE,
30 | allowNull: false,
31 | },
32 | updated_at: {
33 | type: Sequelize.DATE,
34 | allowNull: false,
35 | },
36 | });
37 | },
38 |
39 | down: queryInterface => {
40 | return queryInterface.dropTable('users');
41 | },
42 | };
43 |
--------------------------------------------------------------------------------
/frontend/src/Routes/route.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Redirect, Route } from 'react-router-dom'
3 | import PropTypes from 'prop-types'
4 | import AuthLayout from '../Pages/_layouts/auth'
5 | import DefaultLayout from '../Pages/_layouts/default'
6 |
7 | import { store } from '../store'
8 |
9 | export default function RouteWrapper({
10 | component: Component,
11 | isPrivate,
12 | ...rest
13 | }) {
14 | const { signed } = store.getState().auth
15 |
16 | if (!signed && isPrivate) {
17 | return
18 | }
19 |
20 | if (signed && !isPrivate) {
21 | return
22 | }
23 |
24 | const Layout = signed ? DefaultLayout : AuthLayout
25 |
26 | return (
27 | (
30 |
31 |
32 |
33 | )}
34 | />
35 | )
36 | }
37 |
38 | RouteWrapper.propTypes = {
39 | isPrivate: PropTypes.bool,
40 | component: PropTypes.oneOfType([PropTypes.element, PropTypes.func])
41 | .isRequired,
42 | }
43 |
44 | RouteWrapper.defaultProps = {
45 | isPrivate: false,
46 | }
47 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Registration/Delete/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDispatch } from 'react-redux'
3 | import PropTypes from 'prop-types'
4 | import { Container, Content } from './styles'
5 |
6 | import { DeleteRegistration } from '../../../../store/modules/registration/actions'
7 |
8 | export default function RegistrationDelete({ open, close, regId }) {
9 | const dispatch = useDispatch()
10 |
11 | const handleSubmit = () => {
12 | dispatch(DeleteRegistration({ id: regId }))
13 | }
14 |
15 | return (
16 |
17 |
18 | Você tem certeza disso?
19 |
20 |
23 |
26 |
27 |
28 |
29 | )
30 | }
31 | RegistrationDelete.propTypes = {
32 | open: PropTypes.bool.isRequired,
33 | close: PropTypes.func.isRequired,
34 | regId: PropTypes.arrayOf(PropTypes.number).isRequired,
35 | }
36 |
--------------------------------------------------------------------------------
/backend/src/database/migrations/20191214182657-helporders.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | up: (queryInterface, Sequelize) => {
3 | return queryInterface.createTable('orders', {
4 | id: {
5 | type: Sequelize.INTEGER,
6 | autoIncrement: true,
7 | primaryKey: true,
8 | allowNull: false,
9 | },
10 | student_id: {
11 | type: Sequelize.INTEGER,
12 | references: { model: 'students', key: 'id' },
13 | allowNull: false,
14 | onDelete: 'CASCADE',
15 | },
16 | question: {
17 | type: Sequelize.STRING,
18 | allowNull: false,
19 | },
20 | answer: {
21 | type: Sequelize.STRING,
22 | allowNull: true,
23 | },
24 | answer_at: {
25 | type: Sequelize.DATE,
26 | allowNull: true,
27 | },
28 | created_at: {
29 | type: Sequelize.DATE,
30 | allowNull: false,
31 | },
32 | updated_at: {
33 | type: Sequelize.DATE,
34 | allowNull: false,
35 | },
36 | });
37 | },
38 |
39 | down: queryInterface => {
40 | return queryInterface.dropTable('help_orders');
41 | },
42 | };
43 |
--------------------------------------------------------------------------------
/backend/src/app.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import cors from 'cors';
3 | import helmet from 'helmet';
4 | import redis from 'redis';
5 | import RateLimit from 'express-rate-limit';
6 | import RateLimitRedis from 'rate-limit-redis';
7 | import { resolve } from 'path';
8 | import routes from './routes';
9 | import './database';
10 |
11 | class App {
12 | constructor() {
13 | this.server = express();
14 |
15 | this.middlewares();
16 | this.routes();
17 | }
18 |
19 | middlewares() {
20 | this.server.use(express.json());
21 | this.server.use(cors());
22 | this.server.use(helmet());
23 |
24 | if (process.env.NODE_ENV !== 'development') {
25 | this.server.use(
26 | new RateLimit({
27 | store: new RateLimitRedis({
28 | client: redis.createClient({
29 | host: process.env.REDIS_HOST,
30 | port: process.env.REDIS_PORT,
31 | }),
32 | }),
33 | windowMs: 1000 * 60 * 15,
34 | max: 100,
35 | })
36 | );
37 | }
38 | }
39 |
40 | routes() {
41 | this.server.use(routes);
42 | }
43 | }
44 |
45 | export default new App().server;
46 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/plan/sagas.js:
--------------------------------------------------------------------------------
1 | import { all, call, takeLatest } from 'redux-saga/effects'
2 | import { toast } from 'react-toastify'
3 | import api from '../../../services/api'
4 |
5 | export function* planStore({ payload }) {
6 | try {
7 | const { title, price, duration } = payload
8 | yield call(api.post, 'plan', { title, price, duration })
9 | toast.success('Plano criado com sucesso!!')
10 | window.location.reload()
11 | } catch (err) {
12 | toast.error('Preencha o formulario')
13 | }
14 | }
15 |
16 | export function* planDelete({ payload }) {
17 | yield call(api.delete, `plan/${payload.id}`)
18 |
19 | toast.success('Plano removido')
20 | window.location.reload()
21 | }
22 |
23 | export function* planUpdate({ payload }) {
24 | const { id, title, price, duration } = payload
25 | const profile = { title, price, duration }
26 |
27 | yield call(api.put, `plan/${id}`, profile)
28 |
29 | toast.success('Plano atualizado')
30 | window.location.reload()
31 | }
32 |
33 | export default all([
34 | takeLatest('@plan/REGISTER_PLAN', planStore),
35 | takeLatest('@plan/DELETE_PLAN', planDelete),
36 | takeLatest('@plan/UPDATE_PLAN', planUpdate),
37 | ])
38 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Plan/Delete/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDispatch } from 'react-redux'
3 |
4 | import PropTypes from 'prop-types'
5 | import { Container, Content } from './styles'
6 | import { deletePlan } from '../../../../store/modules/plan/actions'
7 |
8 | export default function PlanDelete({ open, close, planId }) {
9 | const dispatch = useDispatch()
10 |
11 | function handleSubmit({ id }) {
12 | dispatch(deletePlan({ id }))
13 | }
14 |
15 | return (
16 |
17 |
18 | Você tem certeza disso?
19 |
20 |
23 |
30 |
31 |
32 |
33 | )
34 | }
35 | PlanDelete.propTypes = {
36 | open: PropTypes.bool.isRequired,
37 | close: PropTypes.func.isRequired,
38 | planId: PropTypes.arrayOf(PropTypes.number).isRequired,
39 | }
40 |
--------------------------------------------------------------------------------
/frontend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es6: true,
4 | jest: true,
5 | browser: true,
6 | },
7 | extends: ['airbnb', 'prettier', 'prettier/react'],
8 | globals: {
9 | Atomics: 'readonly',
10 | SharedArrayBuffer: 'readonly',
11 | __DEV__: true,
12 | },
13 | parserOptions: {
14 | ecmaFeatures: {
15 | jsx: true,
16 | },
17 | ecmaVersion: 2018,
18 | sourceType: 'module',
19 | },
20 | plugins: ['react', 'jsx-a11y', 'import', 'react-hooks', 'prettier'],
21 | rules: {
22 | 'prettier/prettier': 'error',
23 | 'react/jsx-filename-extension': ['error', { extensions: ['.js', '.jsx'] }],
24 | 'import/prefer-default-export': 'off',
25 | 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
26 | 'react/jsx-one-expression-per-line': 'off',
27 | 'global-require': 'off',
28 | 'react-native/no-raw-text': 'off',
29 | 'no-param-reassign': 'off',
30 | 'no-underscore-dangle': 'off',
31 | 'camelcase': 'off',
32 | 'no-console': ['error', { allow: ['tron'] }],
33 | 'react-hooks/rules-of-hooks': 'error',
34 | 'react-hooks/exhaustive-deps': 'warn',
35 | 'no-useless-concat': "error",
36 |
37 | },
38 | }
39 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Student/Delete/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDispatch } from 'react-redux'
3 | import PropTypes from 'prop-types'
4 | import { Container, Content } from './styles'
5 |
6 | import { deleteStudent } from '../../../../store/modules/student/actions'
7 |
8 | export default function DeleteStudent({ open, close, studentId }) {
9 | const dispatch = useDispatch()
10 |
11 | function handleSubmit({ id }) {
12 | dispatch(deleteStudent({ id }))
13 | }
14 |
15 | return (
16 |
17 |
18 | Você tem certeza disso?
19 |
20 |
23 |
30 |
31 |
32 |
33 | )
34 | }
35 | DeleteStudent.propTypes = {
36 | open: PropTypes.bool.isRequired,
37 | close: PropTypes.func.isRequired,
38 | studentId: PropTypes.arrayOf(PropTypes.string).isRequired,
39 | }
40 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Student/Delete/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components'
2 | import { darken } from 'polished'
3 |
4 | export const Container = styled.div`
5 | top: 0;
6 | left: 0;
7 | width: 100%;
8 | height: 100%;
9 | background: rgba(7, 7, 7, 0.35);
10 | position: absolute;
11 | z-index: 2;
12 | display: ${props => (props.active ? 'block' : 'none')};
13 |
14 | ${props =>
15 | props.close &&
16 | css`
17 | display: none;
18 | `}
19 | `
20 | export const Content = styled.div`
21 | padding: 30px;
22 | width: 350px;
23 | margin: 200px auto;
24 | background: #fff;
25 |
26 | border-bottom-right-radius: 25px;
27 |
28 | box-shadow: 3px 5px rgba(7, 7, 7, 0.5);
29 |
30 | display: flex;
31 | align-items: center;
32 | flex-direction: column;
33 |
34 | strong {
35 | font-size: 25px;
36 | margin-bottom: 15px;
37 | }
38 |
39 | button {
40 | border: 0;
41 | background: none;
42 | margin: 0 15px;
43 | }
44 |
45 | button#button_delete {
46 | border: 0;
47 | background: #dc143c;
48 | color: #fff;
49 | padding: 10px;
50 |
51 | &:hover {
52 | background: ${darken(0.04, '#dc143c')};
53 | }
54 | }
55 | `
56 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Order/Delete/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components'
2 | import { darken } from 'polished'
3 |
4 | export const Container = styled.div`
5 | top: 0;
6 | left: 0;
7 | width: 100%;
8 | height: 1024px;
9 | background: rgba(7, 7, 7, 0.35);
10 | position: absolute;
11 | z-index: 2;
12 | display: ${props => (props.active ? 'block' : 'none')};
13 |
14 | ${props =>
15 | props.close &&
16 | css`
17 | display: none;
18 | `}
19 | `
20 | export const Content = styled.div`
21 | padding: 30px;
22 | width: 350px;
23 |
24 | background: #fff;
25 | margin: 200px auto;
26 |
27 | border-bottom-right-radius: 25px;
28 |
29 | box-shadow: 3px 5px rgba(7, 7, 7, 0.5);
30 |
31 | display: flex;
32 | align-items: center;
33 | flex-direction: column;
34 |
35 | strong {
36 | font-size: 25px;
37 | margin-bottom: 15px;
38 | }
39 |
40 | button {
41 | border: 0;
42 | background: none;
43 | margin: 0 15px;
44 | }
45 |
46 | button#button_delete {
47 | border: 0;
48 | background: #dc143c;
49 | color: #fff;
50 | padding: 10px;
51 |
52 | &:hover {
53 | background: ${darken(0.04, '#dc143c')};
54 | }
55 | }
56 | `
57 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Plan/Delete/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components'
2 | import { darken } from 'polished'
3 |
4 | export const Container = styled.div`
5 | top: 0;
6 | left: 0;
7 | width: 100%;
8 | height: 100%;
9 | background: rgba(7, 7, 7, 0.35);
10 | position: absolute;
11 | z-index: 2;
12 | display: ${props => (props.active ? 'block' : 'none')};
13 |
14 | ${props =>
15 | props.close &&
16 | css`
17 | display: none;
18 | `}
19 | `
20 | export const Content = styled.div`
21 | padding: 30px;
22 | width: 350px;
23 |
24 | background: #fff;
25 | margin: 200px auto;
26 |
27 | border-bottom-right-radius: 25px;
28 |
29 | box-shadow: 3px 5px rgba(7, 7, 7, 0.5);
30 |
31 | display: flex;
32 | align-items: center;
33 | flex-direction: column;
34 |
35 | strong {
36 | font-size: 25px;
37 | margin-bottom: 15px;
38 | }
39 |
40 | button {
41 | border: 0;
42 | background: none;
43 | margin: 0 15px;
44 | }
45 |
46 | button#button_delete {
47 | border: 0;
48 | background: #dc143c;
49 | color: #fff;
50 | padding: 10px;
51 |
52 | &:hover {
53 | background: ${darken(0.04, '#dc143c')};
54 | }
55 | }
56 | `
57 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Registration/Delete/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components'
2 | import { darken } from 'polished'
3 |
4 | export const Container = styled.div`
5 | top: 0;
6 | left: 0;
7 | width: 100%;
8 | height: 100%;
9 | background: rgba(7, 7, 7, 0.35);
10 | position: absolute;
11 | z-index: 2;
12 | display: ${props => (props.active ? 'block' : 'none')};
13 |
14 | ${props =>
15 | props.close &&
16 | css`
17 | display: none;
18 | `}
19 | `
20 | export const Content = styled.div`
21 | padding: 30px;
22 | width: 350px;
23 | margin: 200px auto;
24 | background: #fff;
25 |
26 | border-bottom-right-radius: 25px;
27 |
28 | box-shadow: 3px 5px rgba(7, 7, 7, 0.5);
29 |
30 | display: flex;
31 | align-items: center;
32 | flex-direction: column;
33 |
34 | strong {
35 | font-size: 25px;
36 | margin-bottom: 15px;
37 | }
38 |
39 | button {
40 | border: 0;
41 | background: none;
42 | margin: 0 15px;
43 | }
44 |
45 | button#button_delete {
46 | border: 0;
47 | background: #dc143c;
48 | color: #fff;
49 | padding: 10px;
50 |
51 | &:hover {
52 | background: ${darken(0.04, '#dc143c')};
53 | }
54 | }
55 | `
56 |
--------------------------------------------------------------------------------
/backend/src/lib/Mail.js:
--------------------------------------------------------------------------------
1 | import nodemailer from 'nodemailer';
2 | import { resolve } from 'path';
3 | import nodemailerhbs from 'nodemailer-express-handlebars';
4 | import exphbs from 'express-handlebars';
5 | import mailConfig from '../config/mail';
6 |
7 | class Mail {
8 | constructor() {
9 | const { host, port, secure, auth } = mailConfig;
10 | this.transporter = nodemailer.createTransport({
11 | host,
12 | port,
13 | secure,
14 | auth: auth.user ? auth : null,
15 | });
16 |
17 | this.configureTemplates();
18 | }
19 |
20 | configureTemplates() {
21 | const viewPath = resolve(__dirname, '..', 'app', 'views', 'emails');
22 |
23 | this.transporter.use(
24 | 'compile',
25 | nodemailerhbs({
26 | viewEngine: exphbs.create({
27 | layoutsDir: resolve(viewPath, 'layouts'),
28 | partialsDir: resolve(viewPath, 'partials'),
29 | defaultLayout: 'default',
30 | extname: '.hbs',
31 | }),
32 | viewPath,
33 | extName: '.hbs',
34 | })
35 | );
36 | }
37 |
38 | sendMail(message) {
39 | return this.transporter.sendMail({
40 | ...mailConfig.default,
41 | ...message,
42 | });
43 | }
44 | }
45 | export default new Mail();
46 |
--------------------------------------------------------------------------------
/frontend/src/Pages/SignIn/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDispatch, useSelector } from 'react-redux'
3 | import { Form, Input } from '@rocketseat/unform'
4 |
5 | import { MdEmail } from 'react-icons/md'
6 | import { FaLock } from 'react-icons/fa'
7 | import logo from '../../assets/logoauth.png'
8 | import { signInRequest } from '../../store/modules/auth/actions'
9 |
10 | export default function SignIn() {
11 | const dispatch = useDispatch()
12 | const loading = useSelector(state => state.auth.loading)
13 |
14 | function handleSubmit({ email, password }) {
15 | dispatch(signInRequest(email, password))
16 | }
17 |
18 | return (
19 | <>
20 |
21 |
38 | >
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Order/Register/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDispatch } from 'react-redux'
3 | import { Form, Textarea } from '@rocketseat/unform'
4 | import PropTypes from 'prop-types'
5 | import { Container, Content } from './styles'
6 | import { orderAnswer } from '../../../../store/modules/order/actions'
7 |
8 | export default function Order({ msg, open, close }) {
9 | const dispatch = useDispatch()
10 |
11 | const handleSubmit = ({ answer }) => {
12 | dispatch(orderAnswer({ id: msg.id, answer }))
13 | }
14 |
15 | return (
16 |
17 |
18 | Pergunta do Aluno
19 |
20 | {msg.question}
21 |
22 |
31 |
32 |
33 | )
34 | }
35 | Order.propTypes = {
36 | open: PropTypes.bool.isRequired,
37 | close: PropTypes.func.isRequired,
38 | msg: PropTypes.arrayOf(PropTypes.string).isRequired,
39 | }
40 |
--------------------------------------------------------------------------------
/backend/src/app/Controllers/UserController.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 | import * as Yup from 'yup';
3 | import User from '../models/User';
4 | import authConfig from '../../config/auth';
5 |
6 | class UserController {
7 | async store(req, res) {
8 | const schema = Yup.object().shape({
9 | email: Yup.string()
10 | .email()
11 | .required(),
12 | password: Yup.string().required(),
13 | });
14 | if (!(await schema.isValid(req.body))) {
15 | return res.status(401).json({ error: 'Invalid' });
16 | }
17 |
18 | const { password, email } = req.body;
19 |
20 | const user = await User.findOne({
21 | where: { email },
22 | });
23 |
24 | if (!user) {
25 | return res.status(401).json({ error: 'Usuário não encontrado' });
26 | }
27 |
28 | if (!(await user.checkPassword(password))) {
29 | return res.status(401).json({ error: 'Senha incorreta' });
30 | }
31 |
32 | const { id, name, provider } = user;
33 |
34 | return res.json({
35 | user: {
36 | id,
37 | name,
38 | email,
39 | provider,
40 | token: jwt.sign({ id }, authConfig.secret, {
41 | expiresIn: authConfig.expiresIn,
42 | }),
43 | },
44 | });
45 | }
46 | }
47 |
48 | export default new UserController();
49 |
--------------------------------------------------------------------------------
/backend/src/database/migrations/20191212232840-registrations.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | up: (queryInterface, Sequelize) => {
3 | return queryInterface.createTable('registrations', {
4 | id: {
5 | type: Sequelize.INTEGER,
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | },
10 | student_id: {
11 | type: Sequelize.INTEGER,
12 | references: { model: 'students', key: 'id' },
13 | onDelete: 'SET NULL',
14 | allowNull: false,
15 | },
16 | plan_id: {
17 | type: Sequelize.INTEGER,
18 | references: { model: 'plans', key: 'id' },
19 | onDelete: 'SET NULL',
20 | allowNull: false,
21 | },
22 | start_date: {
23 | type: Sequelize.STRING,
24 | allowNull: false,
25 | },
26 | end_date: {
27 | type: Sequelize.STRING,
28 | allowNull: false,
29 | },
30 | price: {
31 | type: Sequelize.FLOAT,
32 | allowNull: false,
33 | },
34 | created_at: {
35 | type: Sequelize.DATE,
36 | allowNull: false,
37 | },
38 | updated_at: {
39 | type: Sequelize.DATE,
40 | allowNull: false,
41 | },
42 | });
43 | },
44 |
45 | down: queryInterface => {
46 | return queryInterface.dropTable('registrations');
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Registration/Register/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components'
2 | import { lighten } from 'polished'
3 |
4 | export const Container = styled.div`
5 | top: 0;
6 | left: 0;
7 | width: 100%;
8 | height: 100%;
9 | background: rgba(7, 7, 7, 0.35);
10 | position: absolute;
11 | display: ${props => (props.active ? 'block' : 'none')};
12 | z-index: 2;
13 | ${props =>
14 | props.close &&
15 | css`
16 | display: none;
17 | `}
18 | `
19 | export const Content = styled.div`
20 | padding: 30px;
21 | width: 400px;
22 | background: #fff;
23 | margin: 150px auto;
24 | border-bottom-right-radius: 25px;
25 | box-shadow: 3px 5px rgba(7, 7, 7, 0.5);
26 |
27 | form {
28 | margin-top: 15px;
29 | display: flex;
30 | flex-direction: column;
31 |
32 | select {
33 | margin-bottom: 15px;
34 | border: 0;
35 | border-bottom: 1px solid #777;
36 | height: 35px;
37 | padding-left: 10px;
38 | }
39 | div {
40 | margin-top: 35px;
41 | display: flex;
42 | justify-content: space-around;
43 | button {
44 | width: 150px;
45 | border: 0;
46 | background: #dc143c;
47 | height: 35px;
48 | font-weight: bold;
49 | color: #fff;
50 |
51 | &:hover {
52 | background: ${lighten(0.2, '#dc143c')};
53 | }
54 | }
55 | }
56 | }
57 | `
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Aplicação feita para sistema de gerenciamento de academia
7 |
8 | :hammer: Tecnologias utilizadas
9 |
10 | - :hash: NodeJS
11 | - :hash: Express
12 | - :hash: React
13 | - :hash: React(redux)
14 | - :game_die: Sequelize
15 | - :e-mail: Nodemailer
16 | - :e-mail: MailTrap
17 | - :game_die: Redis
18 | - :honeybee: Bee-Queue
19 | - :black_nib: Sucrase
20 | - :pager: Nodemon
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/frontend/src/Pages/_layouts/auth/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import { darken } from 'polished'
3 |
4 | import bgauth from '../../../assets/bgauth.jpg'
5 |
6 | export const Wrapper = styled.div`
7 | width: 100%;
8 | height: 100%;
9 | display: flex;
10 | justify-content: center;
11 | align-items: center;
12 | background: url(${bgauth}) no-repeat;
13 | background-size: cover;
14 | `
15 |
16 | export const Content = styled.div`
17 | width: 100%;
18 | max-width: 350px;
19 |
20 | text-align: center;
21 | padding: 20px;
22 | border-radius: 15px;
23 |
24 | form {
25 | display: flex;
26 | flex-direction: column;
27 | text-align: left;
28 | font-weight: bold;
29 |
30 | div {
31 | display: flex;
32 | align-items: center;
33 |
34 | svg {
35 | margin-right: 5px;
36 | color: #fff;
37 | }
38 |
39 | input {
40 | width: 100%;
41 | border: 0;
42 | border-bottom: 3px solid #777;
43 | padding: 8px;
44 | margin: 10px 0;
45 | }
46 | }
47 |
48 | button {
49 | margin-top: 10px;
50 | background: #e44a68;
51 | border: 0;
52 | border-radius: 4px;
53 | color: #fff;
54 | padding: 10px;
55 |
56 | &:hover {
57 | background: ${darken(0.1, '#e44a68')};
58 | }
59 | }
60 | }
61 |
62 | img {
63 | height: 150px;
64 | margin-bottom: 25px;
65 | }
66 | `
67 |
--------------------------------------------------------------------------------
/backend/src/app/Controllers/CheckinController.js:
--------------------------------------------------------------------------------
1 | import { startOfWeek, endOfWeek } from 'date-fns';
2 | import { Op } from 'sequelize';
3 | import Checkin from '../models/Checkin';
4 | import Student from '../models/Student';
5 |
6 | class CheckinController {
7 | async store(req, res) {
8 | const student = await Student.findByPk(req.params.id);
9 | const actualDate = new Date();
10 |
11 | const verifyCheckins = await Checkin.findAll({
12 | where: {
13 | student_id: req.params.id,
14 | created_at: {
15 | [Op.between]: [startOfWeek(actualDate), endOfWeek(actualDate)],
16 | },
17 | },
18 | });
19 |
20 | if (verifyCheckins.length >= 5) {
21 | return res.status(400).json({
22 | error: 'Você já bateu o limite dessa semana, aproveite para descançar',
23 | });
24 | }
25 |
26 | const checkin = await Checkin.create({
27 | student_id: student.id,
28 | });
29 |
30 | return res.json(checkin);
31 | }
32 |
33 | async index(req, res) {
34 | const count = await Checkin.count();
35 |
36 | const checkin = await Checkin.findAll({
37 | where: { student_id: req.params.id },
38 | attributes: ['id', 'created_at'],
39 | include: [{ model: Student, as: 'student', attributes: ['id', 'name'] }],
40 | });
41 |
42 | res.header('X-Total-Count', count);
43 |
44 | return res.json(checkin);
45 | }
46 | }
47 | export default new CheckinController();
48 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/auth/sagas.js:
--------------------------------------------------------------------------------
1 | import { takeLatest, put, call, all } from 'redux-saga/effects'
2 | import { toast } from 'react-toastify'
3 |
4 | import api from '../../../services/api'
5 | import history from '../../../services/history'
6 | import { signInSuccess, signFailure } from './actions'
7 |
8 | export function* SignIn({ payload }) {
9 | try {
10 | const { email, password } = payload
11 |
12 | const response = yield call(api.post, 'session', {
13 | email,
14 | password,
15 | })
16 |
17 | const { token } = response.data.user
18 | const { user } = response.data
19 |
20 | if (!user.provider) {
21 | toast.error('Usuario nao é prestador')
22 | }
23 |
24 | api.defaults.headers.Authorization = `Bearer ${token}`
25 |
26 | yield put(signInSuccess(token, user))
27 |
28 | history.push('/students')
29 | } catch (err) {
30 | toast.error('Falha na autenticação')
31 | yield put(signFailure())
32 | }
33 | }
34 |
35 | export function setToken({ payload }) {
36 | if (!payload) return
37 |
38 | const { token } = payload.auth
39 |
40 | if (token) {
41 | api.defaults.headers.Authorization = `Bearer ${token}`
42 | }
43 | }
44 |
45 | export function signOut() {
46 | toast.success('Até mais')
47 | }
48 |
49 | export default all([
50 | takeLatest('persist/REHYDRATE', setToken),
51 | takeLatest('@auth/SIGN_IN_REQUEST', SignIn),
52 | takeLatest('@auth/SIGN_OUT', signOut),
53 | ])
54 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Plan/Register/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDispatch } from 'react-redux'
3 | import { Form, Input } from '@rocketseat/unform'
4 | import PropTypes from 'prop-types'
5 |
6 | import { Container, Content } from './styles'
7 | import { registerPlan } from '../../../../store/modules/plan/actions'
8 |
9 | export default function PlanRegister({ open, close }) {
10 | const dispatch = useDispatch()
11 |
12 | const handleSubmit = ({ title, price, duration }) => {
13 | dispatch(registerPlan({ title, price, duration }))
14 | }
15 |
16 | return (
17 |
18 |
19 | Cadastrar plano
20 | Por favor, preencha este formulário para criar um plano.
21 |
22 |
23 |
37 |
38 |
39 | )
40 | }
41 | PlanRegister.propTypes = {
42 | open: PropTypes.bool.isRequired,
43 | close: PropTypes.func.isRequired,
44 | }
45 |
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gympoint",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "dev": "nodemon src/server.js",
8 | "queue": "nodemon src/queue.js"
9 | },
10 | "dependencies": {
11 | "bcryptjs": "^2.4.3",
12 | "bee-queue": "^1.2.2",
13 | "blob-stream": "^0.1.3",
14 | "cors": "^2.8.5",
15 | "crypto": "^1.0.1",
16 | "date-fns": "^2.8.1",
17 | "dotenv": "^8.2.0",
18 | "express": "^4.17.1",
19 | "express-brute": "^1.0.1",
20 | "express-brute-redis": "^0.0.1",
21 | "express-handlebars": "^3.1.0",
22 | "express-rate-limit": "^5.1.1",
23 | "handlebars": "^4.5.3",
24 | "helmet": "^3.21.3",
25 | "jsonwebtoken": "^8.5.1",
26 | "kue": "^0.11.6",
27 | "multer": "^1.4.2",
28 | "nodemailer": "^6.4.2",
29 | "nodemailer-express-handlebars": "^3.1.0",
30 | "numeral": "^2.0.6",
31 | "pdfkit": "^0.11.0",
32 | "pg": "^7.14.0",
33 | "pg-hstore": "^2.3.3",
34 | "rate-limit-redis": "^1.7.0",
35 | "redis": "^3.0.2",
36 | "sequelize": "^5.21.2",
37 | "yup": "^0.27.0"
38 | },
39 | "devDependencies": {
40 | "eslint": "^6.6.0",
41 | "eslint-config-airbnb-base": "^14.0.0",
42 | "eslint-config-prettier": "^6.7.0",
43 | "eslint-plugin-import": "^2.18.2",
44 | "eslint-plugin-prettier": "^3.1.1",
45 | "nodemon": "^2.0.1",
46 | "prettier": "^1.19.1",
47 | "sequelize-cli": "^5.5.1",
48 | "sucrase": "^3.10.1"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Plan/Update/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDispatch } from 'react-redux'
3 | import { Form, Input } from '@rocketseat/unform'
4 | import PropTypes from 'prop-types'
5 | import { Container, Content } from './styles'
6 |
7 | import { updatePlan } from '../../../../store/modules/plan/actions'
8 |
9 | export default function PlanUpdate({ open, close, plan }) {
10 | const dispatch = useDispatch()
11 |
12 | function handleSubmit({ title, price, duration }) {
13 | dispatch(
14 | updatePlan({
15 | id: plan.id,
16 | title,
17 | price,
18 | duration,
19 | })
20 | )
21 | }
22 |
23 | return (
24 |
25 |
26 | Atualizar dados
27 |
39 |
40 |
41 | )
42 | }
43 | PlanUpdate.propTypes = {
44 | open: PropTypes.bool.isRequired,
45 | close: PropTypes.func.isRequired,
46 | plan: PropTypes.arrayOf(PropTypes.string).isRequired,
47 | }
48 |
--------------------------------------------------------------------------------
/backend/src/app/Controllers/GymOrderController.js:
--------------------------------------------------------------------------------
1 | import * as Yup from 'yup';
2 | import Order from '../models/Order';
3 | import Student from '../models/Student';
4 | import Queue from '../../lib/Queue';
5 | import AnswerMail from '../jobs/AnswerMail';
6 |
7 | class GymOrderController {
8 | async store(req, res) {
9 | const schema = Yup.object().shape({
10 | answer: Yup.string().required(),
11 | });
12 |
13 | if (!(await schema.isValid(req.body))) {
14 | return res.status(400).json({ error: 'Erro na validação' });
15 | }
16 |
17 | const { answer } = req.body;
18 | const orders = await Order.findByPk(req.params.id);
19 |
20 | const { email } = await Student.findByPk(orders.student_id);
21 |
22 | const showOrders = await orders.update({
23 | answer,
24 | answer_at: new Date(),
25 | });
26 |
27 | await Queue.add(AnswerMail.key, { answer, email });
28 |
29 | return res.json(showOrders);
30 | }
31 |
32 | async index(req, res) {
33 | const { question } = req.query;
34 | const showOrders = await Order.findAll({
35 | attributes: ['id', 'question', 'answer', 'answer_at'],
36 | include: [{ model: Student, as: 'student', attributes: ['id', 'name'] }],
37 | limit: 5,
38 | offset: question,
39 | });
40 |
41 | return res.json(showOrders);
42 | }
43 |
44 | async delete(req, res) {
45 | await Order.destroy({ where: { id: req.params.id } });
46 | return res.json();
47 | }
48 | }
49 | export default new GymOrderController();
50 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Student/Update/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components'
2 | import { lighten } from 'polished'
3 |
4 | export const Container = styled.div`
5 | top: 0;
6 | left: 0;
7 | width: 100%;
8 | height: 100%;
9 | background: rgba(7, 7, 7, 0.35);
10 | position: absolute;
11 | z-index: 2;
12 | display: ${props => (props.active ? 'block' : 'none')};
13 |
14 | ${props =>
15 | props.close &&
16 | css`
17 | display: none;
18 | `}
19 | `
20 | export const Content = styled.div`
21 | padding: 30px;
22 | width: 400px;
23 | margin: 50px auto;
24 | background: #fff;
25 |
26 | border-bottom-right-radius: 25px;
27 |
28 | box-shadow: 3px 5px rgba(7, 7, 7, 0.5);
29 |
30 | > button {
31 | border: 0;
32 | background: none;
33 | }
34 |
35 | form {
36 | margin-top: 15px;
37 | display: flex;
38 | flex-direction: column;
39 |
40 | input {
41 | margin-bottom: 15px;
42 | border: 0;
43 | border-bottom: 1px solid #777;
44 | height: 35px;
45 | padding-left: 10px;
46 |
47 | &::placeholder {
48 | padding: 5px;
49 | font-size: 10px;
50 | }
51 | }
52 |
53 | div {
54 | margin-top: 35px;
55 | display: flex;
56 | justify-content: space-around;
57 | button {
58 | width: 150px;
59 | border: 0;
60 | background: #dc143c;
61 | height: 35px;
62 | font-weight: bold;
63 | color: #fff;
64 |
65 | &:hover {
66 | background: ${lighten(0.2, '#dc143c')};
67 | }
68 | }
69 | }
70 | }
71 | `
72 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Plan/Update/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components'
2 | import { lighten } from 'polished'
3 |
4 | export const Container = styled.div`
5 | top: 0;
6 | left: 0;
7 | width: 100%;
8 | height: 100%;
9 | background: rgba(7, 7, 7, 0.35);
10 | position: absolute;
11 | z-index: 2;
12 | display: ${props => (props.active ? 'block' : 'none')};
13 |
14 | ${props =>
15 | props.close &&
16 | css`
17 | display: none;
18 | `}
19 | `
20 | export const Content = styled.div`
21 | padding: 30px;
22 | width: 400px;
23 | margin: 100px auto;
24 | background: #fff;
25 |
26 | overflow: hidden;
27 | border-bottom-right-radius: 25px;
28 |
29 | box-shadow: 3px 5px rgba(7, 7, 7, 0.5);
30 |
31 | > button {
32 | border: 0;
33 | background: none;
34 | }
35 |
36 | form {
37 | margin-top: 15px;
38 | display: flex;
39 | flex-direction: column;
40 |
41 | input {
42 | margin-bottom: 15px;
43 | border: 0;
44 | border-bottom: 1px solid #777;
45 | height: 35px;
46 | padding-left: 10px;
47 |
48 | &::placeholder {
49 | padding: 5px;
50 | font-size: 10px;
51 | }
52 | }
53 |
54 | div {
55 | margin-top: 35px;
56 | display: flex;
57 | justify-content: space-around;
58 | button {
59 | width: 150px;
60 | border: 0;
61 | background: #dc143c;
62 | height: 35px;
63 | font-weight: bold;
64 | color: #fff;
65 |
66 | &:hover {
67 | background: ${lighten(0.2, '#dc143c')};
68 | }
69 | }
70 | }
71 | }
72 | `
73 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Plan/Register/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components'
2 | import { lighten } from 'polished'
3 |
4 | export const Container = styled.div`
5 | top: 0;
6 | left: 0;
7 | width: 100%;
8 | height: 100%;
9 | background: rgba(7, 7, 7, 0.35);
10 | position: absolute;
11 | z-index: 2;
12 | display: ${props => (props.active ? 'block' : 'none')};
13 |
14 | ${props =>
15 | props.close &&
16 | css`
17 | display: none;
18 | `}
19 | `
20 | export const Content = styled.div`
21 | padding: 30px;
22 | width: 400px;
23 | margin: 100px auto;
24 | background: #fff;
25 |
26 | overflow: hidden;
27 | border-bottom-right-radius: 25px;
28 |
29 | box-shadow: 3px 5px rgba(7, 7, 7, 0.5);
30 |
31 | h1 {
32 | margin-bottom: 25px;
33 | }
34 |
35 | hr {
36 | margin-bottom: 20px;
37 | }
38 |
39 | form {
40 | margin-top: 15px;
41 | display: flex;
42 | flex-direction: column;
43 |
44 | input {
45 | margin-bottom: 15px;
46 | border: 0;
47 | border-bottom: 1px solid #777;
48 | height: 35px;
49 | padding-left: 10px;
50 |
51 | &::placeholder {
52 | padding: 5px;
53 | font-size: 10px;
54 | }
55 | }
56 |
57 | div {
58 | margin-top: 35px;
59 | display: flex;
60 | justify-content: space-around;
61 | button {
62 | width: 150px;
63 | border: 0;
64 | background: #dc143c;
65 | height: 35px;
66 | font-weight: bold;
67 | color: #fff;
68 |
69 | &:hover {
70 | background: ${lighten(0.2, '#dc143c')};
71 | }
72 | }
73 | }
74 | }
75 | `
76 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Order/Register/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components'
2 | import { lighten } from 'polished'
3 |
4 | export const Container = styled.div`
5 | top: 0;
6 | left: 0;
7 | width: 100%;
8 | height: 100%;
9 | background: rgba(7, 7, 7, 0.5);
10 | position: absolute;
11 | z-index: 2;
12 | display: ${props => (props.active ? 'block' : 'none')};
13 |
14 | ${props =>
15 | props.close &&
16 | css`
17 | display: none;
18 | `}
19 | `
20 | export const Content = styled.div`
21 | padding: 30px;
22 | width: 400px;
23 | margin: 100px auto;
24 | background: #fff;
25 |
26 | overflow: hidden;
27 | border-bottom-right-radius: 25px;
28 |
29 | box-shadow: 3px 5px rgba(7, 7, 7, 0.5);
30 |
31 | p {
32 | margin-top: 15px;
33 | padding: 10px;
34 | font-weight: bold;
35 | color: #fff;
36 | border-radius: 10px;
37 | margin-bottom: 15px;
38 | background: #dc143c;
39 | }
40 |
41 | form {
42 | display: flex;
43 | align-items: center;
44 | flex-direction: column;
45 |
46 | textarea {
47 | border: 0;
48 | padding: 10px;
49 | background: #eee;
50 | height: 100px;
51 | width: 100%;
52 | margin-bottom: 15px;
53 | }
54 |
55 | div {
56 | width: 100%;
57 | display: flex;
58 | justify-content: space-between;
59 |
60 | button {
61 | border: 0;
62 | background: #dc143c;
63 | height: 40px;
64 | width: 150px;
65 | font-weight: bold;
66 | color: #fff;
67 |
68 | &:hover {
69 | background: ${lighten(0.2, '#dc143c')};
70 | }
71 | }
72 | }
73 | }
74 | `
75 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Student/Register/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components'
2 | import { lighten } from 'polished'
3 |
4 | export const Container = styled.div`
5 | top: 0;
6 | left: 0;
7 | width: 100%;
8 | height: 100%;
9 | background: rgba(7, 7, 7, 0.35);
10 | position: absolute;
11 | display: ${props => (props.active ? 'block' : 'none')};
12 | z-index: 2;
13 | ${props =>
14 | props.close &&
15 | css`
16 | display: none;
17 | `}
18 | `
19 | export const Content = styled.div`
20 | padding: 30px;
21 | width: 400px;
22 | margin: 50px auto;
23 | background: #fff;
24 | border-bottom-right-radius: 25px;
25 |
26 | box-shadow: 3px 5px rgba(7, 7, 7, 0.5);
27 |
28 | > button {
29 | border: 0;
30 | background: none;
31 | }
32 |
33 | h1 {
34 | margin-bottom: 25px;
35 | }
36 |
37 | hr {
38 | margin-bottom: 20px;
39 | }
40 |
41 | form {
42 | display: flex;
43 | flex-direction: column;
44 |
45 | input {
46 | margin-bottom: 15px;
47 | border: 0;
48 | border-bottom: 1px solid #777;
49 | height: 35px;
50 | padding-left: 10px;
51 |
52 | &::placeholder {
53 | padding: 5px;
54 | font-size: 10px;
55 | }
56 | }
57 |
58 | div {
59 | margin-top: 35px;
60 | display: flex;
61 | justify-content: space-around;
62 | button {
63 | width: 150px;
64 | border: 0;
65 | background: #dc143c;
66 | height: 35px;
67 | font-weight: bold;
68 | color: #fff;
69 |
70 | &:hover {
71 | background: ${lighten(0.2, '#dc143c')};
72 | }
73 | }
74 | }
75 | }
76 | `
77 |
--------------------------------------------------------------------------------
/frontend/src/Pages/HelpOrders/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Container = styled.div`
4 | width: 100%;
5 | margin: 20px auto;
6 | max-width: 1500px;
7 | height: 468px;
8 | `
9 |
10 | export const Header = styled.header`
11 | margin-top: 100px;
12 | padding: 20px 50px;
13 | display: flex;
14 | height: 150px;
15 | width: 800px;
16 | margin: 0 auto;
17 |
18 | div {
19 | display: flex;
20 | align-items: center;
21 |
22 | strong {
23 | font-size: 40px;
24 | color: #fff;
25 | margin-right: 20px;
26 | margin-left: 5px;
27 | }
28 | }
29 | `
30 |
31 | export const Content = styled.div`
32 | ul {
33 | width: 800px;
34 | margin: 0 auto;
35 | }
36 | `
37 |
38 | export const Li = styled.li`
39 | display: flex;
40 | align-items: center;
41 | padding: 10px;
42 | background: #fff;
43 | border-radius: 10px;
44 | margin-bottom: 10px;
45 | border-left: ${props =>
46 | props.hasUnread ? '7px solid #dc143c' : '7px solid #00FF00'};
47 | div {
48 | width: 600px;
49 | display: flex;
50 | align-items: center;
51 |
52 | img {
53 | height: 50px;
54 | width: 50px;
55 | border-radius: 50%;
56 | margin-right: 10px;
57 | }
58 |
59 | strong {
60 | font-weight: bold;
61 | color: #777;
62 | }
63 |
64 | p {
65 | margin-top: 10px;
66 | }
67 | }
68 |
69 | button {
70 | display: flex;
71 | align-items: center;
72 | flex-direction: column;
73 | justify-content: center;
74 |
75 | border: 0;
76 | background: 0;
77 | margin: 0 15px;
78 |
79 | small {
80 | color: #777;
81 | }
82 |
83 | svg {
84 | color: #777;
85 | }
86 | }
87 | `
88 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/student/sagas.js:
--------------------------------------------------------------------------------
1 | import { call, all, takeLatest, put } from 'redux-saga/effects'
2 | import { toast } from 'react-toastify'
3 | import api from '../../../services/api'
4 | import { findStudentSuccess } from './actions'
5 |
6 | export function* studentStore({ payload }) {
7 | try {
8 | const { name, email, age, weight, height } = payload
9 |
10 | yield call(api.post, 'student', {
11 | name,
12 | email,
13 | age,
14 | weight,
15 | height,
16 | })
17 |
18 | toast.success('Cadastro realizado com sucesso!!')
19 | window.location.reload()
20 | } catch (err) {
21 | toast.error('Preencha os campos')
22 | }
23 | }
24 |
25 | export function* studentDelete({ payload }) {
26 | const { id } = payload
27 | yield call(api.delete, `student/${id}`)
28 | toast.success('Usuario deletado com sucesso!!')
29 | window.location.reload()
30 | }
31 |
32 | export function* studentUpdate({ payload }) {
33 | const { id, name, email, age, weight, height } = payload
34 |
35 | const profile = { id, name, email, age, weight, height }
36 |
37 | yield call(api.put, `student/${id}`, profile)
38 |
39 | toast.success('Perfil atualizado com sucesso!!!')
40 | window.location.reload()
41 | }
42 |
43 | export function* studentShow({ payload }) {
44 | const { name } = payload
45 |
46 | const response = yield call(api.get, `student?name=${name}`)
47 |
48 | yield put(findStudentSuccess(response.data))
49 | }
50 |
51 | export default all([
52 | takeLatest('@student/SIGNUP_STUDENT_REQUEST', studentStore),
53 | takeLatest('@student/DELETE_STUDENT', studentDelete),
54 | takeLatest('@student/UPDATE_STUDENT_REQUEST', studentUpdate),
55 | takeLatest('@student/FIND_STUDENT', studentShow),
56 | ])
57 |
--------------------------------------------------------------------------------
/backend/src/app/Controllers/PlanController.js:
--------------------------------------------------------------------------------
1 | import * as Yup from 'yup';
2 |
3 | import Plan from '../models/Plan';
4 |
5 | class PlanController {
6 | async index(req, res) {
7 | const plan = await Plan.findAll({
8 | attributes: ['id', 'title', 'price', 'duration'],
9 | order: ['duration'],
10 | where: { deleted: false },
11 | });
12 | return res.json(plan);
13 | }
14 |
15 | async store(req, res) {
16 | const schema = Yup.object().shape({
17 | title: Yup.string().required(),
18 | price: Yup.number().required(),
19 | duration: Yup.number().required(),
20 | });
21 |
22 | if (!(await schema.isValid(req.body))) {
23 | return res.status(400).json({ error: 'Erro na validação' });
24 | }
25 | const { title, price, duration } = req.body;
26 |
27 | const checkPlan = await Plan.findOne({ where: { title } });
28 |
29 | if (checkPlan) {
30 | return res.status(400).json({ error: 'Este plano já existe' });
31 | }
32 |
33 | Plan.create({ ...req.body, deleted: false });
34 |
35 | return res.json({
36 | title,
37 | price,
38 | duration: `${duration} days`,
39 | });
40 | }
41 |
42 | async update(req, res) {
43 | const schema = Yup.object().shape({
44 | title: Yup.string(),
45 | price: Yup.string(),
46 | duration: Yup.string(),
47 | });
48 |
49 | if (!(await schema.isValid(req.body))) {
50 | return res.status(400).json({ error: 'Erro na validação' });
51 | }
52 |
53 | const plan = await Plan.findByPk(req.params.planId);
54 |
55 | const { duration, price, title } = await plan.update(req.body);
56 |
57 | return res.json({ title, duration, price });
58 | }
59 |
60 | async delete(req, res) {
61 | const plan = await Plan.findByPk(req.params.planId);
62 | await plan.update({ deleted: true });
63 | return res.json();
64 | }
65 | }
66 |
67 | export default new PlanController();
68 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Student/Update/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDispatch } from 'react-redux'
3 | import PropTypes from 'prop-types'
4 | import { Form, Input } from '@rocketseat/unform'
5 | import { Container, Content } from './styles'
6 | import { updateStudentRequest } from '../../../../store/modules/student/actions'
7 |
8 | export default function StudentUpdate({ open, close, student }) {
9 | const dispatch = useDispatch()
10 |
11 | function handleSubmit({ name, email, age, weight, height }) {
12 | dispatch(
13 | updateStudentRequest({
14 | id: student.id,
15 | name,
16 | email,
17 | age,
18 | weight,
19 | height,
20 | })
21 | )
22 | }
23 |
24 | return (
25 |
26 |
27 | Atualizar dados
28 |
56 |
57 |
58 | )
59 | }
60 | StudentUpdate.propTypes = {
61 | open: PropTypes.bool.isRequired,
62 | close: PropTypes.func.isRequired,
63 | student: PropTypes.arrayOf(PropTypes.string).isRequired,
64 | }
65 |
--------------------------------------------------------------------------------
/frontend/src/components/Header/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useMemo } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { useDispatch } from 'react-redux'
4 |
5 | import { IoMdLogOut } from 'react-icons/io'
6 | import logo from '../../assets/icon.png'
7 |
8 | import { Container, Content, Notification } from './styles'
9 | import api from '../../services/api'
10 | import { signOut } from '../../store/modules/auth/actions'
11 |
12 | export default function Header() {
13 | const dispatch = useDispatch()
14 | const [questions, setQuestions] = useState([])
15 | const [count, setCount] = useState(0)
16 |
17 | const hasUnread = useMemo(() => questions.find(q => q.answer === null), [
18 | questions,
19 | ])
20 |
21 | useEffect(() => {
22 | const response = questions.filter(question => question.answer === null)
23 | setCount(response.length)
24 | }, [questions])
25 |
26 | useEffect(() => {
27 | async function loadQuestion() {
28 | const response = await api.get('help-orders')
29 |
30 | setQuestions(response.data)
31 | }
32 | loadQuestion()
33 | }, [])
34 |
35 | function handleSignOut() {
36 | dispatch(signOut())
37 | }
38 |
39 | return (
40 |
41 |
42 |
43 |

44 |
45 |
58 |
64 |
65 |
66 | )
67 | }
68 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wolfzz",
3 | "version": "0.1.0",
4 | "private": false,
5 | "dependencies": {
6 | "@material-ui/core": "^4.9.3",
7 | "@rocketseat/unform": "^1.6.2",
8 | "@testing-library/jest-dom": "^4.2.4",
9 | "@testing-library/react": "^9.3.2",
10 | "@testing-library/user-event": "^7.1.2",
11 | "axios": "^0.19.2",
12 | "babel-plugin-root-import": "^6.4.1",
13 | "cli-truncate": "^2.1.0",
14 | "customize-cra": "^0.9.1",
15 | "date-fns": "^2.12.0",
16 | "eslint-config-prettier": "^6.10.0",
17 | "eslint-plugin-prettier": "^3.1.2",
18 | "formik": "^2.1.4",
19 | "history": "^4.10.1",
20 | "immer": "^5.3.6",
21 | "material-table": "^1.57.2",
22 | "polished": "^3.4.4",
23 | "prettier": "^1.19.1",
24 | "prop-types": "^15.7.2",
25 | "react": "^16.12.0",
26 | "react-app-rewired": "^2.1.5",
27 | "react-dom": "^16.12.0",
28 | "react-icons": "^3.9.0",
29 | "react-number-format": "^4.4.1",
30 | "react-perfect-scrollbar": "^1.5.8",
31 | "react-redux": "^7.1.3",
32 | "react-router-dom": "^5.1.2",
33 | "react-scripts": "3.4.0",
34 | "react-toastify": "^5.5.0",
35 | "reactotron-react-js": "^3.3.7",
36 | "reactotron-redux": "^3.1.2",
37 | "reactotron-redux-saga": "^4.2.3",
38 | "redux": "^4.0.5",
39 | "redux-persist": "^6.0.0",
40 | "redux-saga": "^1.1.3",
41 | "styled-components": "^5.0.1",
42 | "yup": "^0.28.3"
43 | },
44 | "scripts": {
45 | "start": "react-scripts start",
46 | "build": "react-scripts build",
47 | "test": "react-scripts test",
48 | "eject": "react-scripts eject"
49 | },
50 | "browserslist": {
51 | "production": [
52 | ">0.2%",
53 | "not dead",
54 | "not op_mini all"
55 | ],
56 | "development": [
57 | "last 1 chrome version",
58 | "last 1 firefox version",
59 | "last 1 safari version"
60 | ]
61 | },
62 | "devDependencies": {
63 | "eslint": "^6.8.0",
64 | "eslint-config-airbnb": "^18.0.1",
65 | "eslint-plugin-import": "^2.20.1",
66 | "eslint-plugin-jsx-a11y": "^6.2.3",
67 | "eslint-plugin-react": "^7.18.3",
68 | "eslint-plugin-react-hooks": "^1.7.0"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Registration/Register/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useMemo } from 'react'
2 | import { useDispatch } from 'react-redux'
3 | import PropTypes from 'prop-types'
4 | import { Form, Select } from '@rocketseat/unform'
5 | import { Container, Content } from './styles'
6 | import api from '../../../../services/api'
7 | import { RegisterStore } from '../../../../store/modules/registration/actions'
8 |
9 | export default function RegistrationStore({ open, close }) {
10 | const dispatch = useDispatch()
11 | const [students, setStudents] = useState([])
12 | const [plans, setPlans] = useState([])
13 |
14 | useMemo(async () => {
15 | const response = await api.get('/plan')
16 |
17 | const planData = response.data.map(data => {
18 | return {
19 | id: data.id,
20 | title: data.title,
21 | }
22 | })
23 |
24 | setPlans(planData)
25 | }, [])
26 |
27 | useMemo(async () => {
28 | const response = await api.get(`/students`)
29 |
30 | const studentData = response.data.map(student => {
31 | return {
32 | id: student.id,
33 | title: student.name,
34 | }
35 | })
36 | setStudents(studentData)
37 | }, [])
38 |
39 | const handleSubmit = ({ student_id, plan_id }) => {
40 | dispatch(RegisterStore({ student_id, plan_id }))
41 | }
42 |
43 | return (
44 |
45 |
46 | Matricular aluno
47 |
48 |
66 |
67 |
68 | )
69 | }
70 |
71 | RegistrationStore.propTypes = {
72 | open: PropTypes.bool.isRequired,
73 | close: PropTypes.func.isRequired,
74 | }
75 |
--------------------------------------------------------------------------------
/backend/src/app/Controllers/StudentController.js:
--------------------------------------------------------------------------------
1 | import * as Yup from 'yup';
2 | import { Op } from 'sequelize';
3 | import Student from '../models/Student';
4 |
5 | class StudentController {
6 | async store(req, res) {
7 | const schema = Yup.object().shape({
8 | name: Yup.string()
9 | .required()
10 | .min(3),
11 | email: Yup.string()
12 | .email()
13 | .required(),
14 | age: Yup.number()
15 | .required()
16 | .positive(),
17 | weight: Yup.number()
18 | .required()
19 | .positive(),
20 | height: Yup.number()
21 | .required()
22 | .positive(),
23 | });
24 |
25 | if (!(await schema.isValid(req.body))) {
26 | return res.status(400).json({ error: 'Erro na validação' });
27 | }
28 |
29 | const { email } = req.body;
30 |
31 | const userExists = await Student.findOne({
32 | where: { email },
33 | });
34 |
35 | if (userExists) {
36 | return res.status(400).json({ error: 'Este usuário já existe' });
37 | }
38 |
39 | const student = await Student.create(req.body);
40 |
41 | return res.json(student);
42 | }
43 |
44 | async update(req, res) {
45 | const schema = Yup.object().shape({
46 | name: Yup.string().min(3),
47 | email: Yup.string().email(),
48 | weight: Yup.number().positive(),
49 | height: Yup.number().positive(),
50 | age: Yup.number().positive(),
51 | });
52 | if (!(await schema.isValid(req.body))) {
53 | return res.status(400).json({ error: 'Erro na validação' });
54 | }
55 | const student = await Student.findByPk(req.params.id);
56 | const result = await student.update(req.body);
57 |
58 | return res.json(result);
59 | }
60 |
61 | async show(req, res) {
62 | const students = await Student.findAll({ order: ['name'] });
63 | return res.json(students);
64 | }
65 |
66 | async delete(req, res) {
67 | await Student.destroy({ where: { id: req.params.id } });
68 | return res.json();
69 | }
70 |
71 | async index(req, res) {
72 | const { name } = req.query;
73 | const student = await Student.findAll({
74 | where: { name: { [Op.startsWith]: `${name}%` } },
75 | });
76 |
77 | return res.json(student);
78 | }
79 | }
80 |
81 | export default new StudentController();
82 |
--------------------------------------------------------------------------------
/frontend/src/components/Header/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components'
2 |
3 | export const Container = styled.div`
4 | display: flex;
5 | justify-content: center;
6 | background: rgba(7, 7, 7, 0.35);
7 | border-bottom: 1px solid #777;
8 | width: 100%;
9 | height: 48px;
10 | `
11 | export const Content = styled.div`
12 | width: 100%;
13 | max-width: 1500px;
14 | display: flex;
15 | justify-content: space-around;
16 |
17 | > div {
18 | display: flex;
19 | align-items: center;
20 | img {
21 | height: 40px;
22 | }
23 | }
24 |
25 | nav {
26 | display: flex;
27 |
28 | align-items: center;
29 |
30 | a {
31 | color: #fff;
32 | padding: 0 5px;
33 | font-weight: bold;
34 | margin: 0 10px;
35 | position: relative;
36 | padding: 14px 15px;
37 | text-align: center;
38 |
39 | &:hover {
40 | background: linear-gradient(
41 | to bottom,
42 | rgba(255, 0, 0, 0),
43 | rgba(214, 106, 106, 1)
44 | );
45 | color: #fff;
46 | border-bottom: 3px solid #e44a68;
47 | }
48 |
49 | div {
50 | display: flex;
51 | flex-direction: row;
52 |
53 | small {
54 | text-align: center;
55 | }
56 | }
57 | }
58 | }
59 |
60 | aside {
61 | display: flex;
62 | align-items: center;
63 | justify-content: center;
64 |
65 | strong {
66 | color: #fff;
67 | border-right: 1px solid #777;
68 | padding-right: 10px;
69 | }
70 |
71 | a {
72 | margin-left: 10px;
73 | color: #e44a68;
74 | display: flex;
75 | flex-direction: row;
76 | align-items: center;
77 | justify-content: center;
78 | svg {
79 | height: 20px;
80 | width: 20px;
81 | }
82 | }
83 | }
84 | `
85 | export const Notification = styled.div`
86 | width: 17px;
87 | height: 17px;
88 | background: #e44a68;
89 | border-radius: 50%;
90 | margin-left: 6px;
91 | display: flex;
92 | align-items: center;
93 | justify-content: center;
94 | font-size: 12px;
95 |
96 | ${props =>
97 | !props.hasUnread &&
98 | css`
99 | visibility: hidden;
100 | `}
101 | `
102 |
--------------------------------------------------------------------------------
/frontend/src/components/Modals/Student/Register/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { useDispatch } from 'react-redux'
4 | import { Form, Input } from '@rocketseat/unform'
5 | import { MdAddAPhoto } from 'react-icons/md'
6 | import { Container, Content } from './styles'
7 | import { studentSignUp } from '../../../../store/modules/student/actions'
8 | import api from '../../../../services/api'
9 |
10 | export default function StudentSignUp({ open, close }) {
11 | const dispatch = useDispatch()
12 | const [url, setUrl] = useState(null)
13 | const [fileField, setFileField] = useState(null)
14 |
15 | function handleSubmit({ avatar, name, email, age, weight, height }) {
16 | dispatch(studentSignUp({ avatar, name, email, age, weight, height }))
17 | }
18 |
19 | const handleChange = async e => {
20 | e.preventDefault()
21 | const data = new FormData()
22 | data.append('file', e.target.files[0])
23 | const response = await api.post('/files', data)
24 | setUrl(response.data.url)
25 | setFileField(response.data.id)
26 | }
27 |
28 | return (
29 |
30 |
31 | Cadastrar aluno
32 | Por favor, preencha este formulário para criar uma conta.
33 |
34 |
62 |
63 |
64 | )
65 | }
66 |
67 | StudentSignUp.propTypes = {
68 | open: PropTypes.bool.isRequired,
69 | close: PropTypes.func.isRequired,
70 | }
71 |
--------------------------------------------------------------------------------
/frontend/src/Pages/HelpOrders/index.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo, useState } from 'react'
2 |
3 | import { TiMessage } from 'react-icons/ti'
4 | import { FaRegTimesCircle } from 'react-icons/fa'
5 | import { Header, Content, Container, Li } from './styles'
6 |
7 | import api from '../../services/api'
8 | import { Truncate } from '../../util/util'
9 | import ModalMsg from '../../components/Modals/Order/Register'
10 | import ModalDelete from '../../components/Modals/Order/Delete'
11 |
12 | export default function HelpOrders() {
13 | const [questions, setQuestions] = useState([])
14 | const [questionId, setQuestionId] = useState('')
15 | const [question, setQuestion] = useState([])
16 |
17 | const [open, setOpen] = useState(false)
18 | const [Modaldelete, setModalDelete] = useState(false)
19 |
20 | useMemo(async () => {
21 | const response = await api.get('help-orders')
22 | setQuestions(response.data)
23 | }, [])
24 |
25 | return (
26 |
27 | setOpen(false)} />
28 | setModalDelete(false)}
31 | planId={questionId}
32 | />
33 |
38 |
39 |
40 | {questions.map(quest => (
41 | -
42 |
43 |
44 | {quest.student.name}
45 | {Truncate(quest.question, 70)}
46 |
47 |
48 |
58 |
68 |
69 | ))}
70 |
71 |
72 |
73 | )
74 | }
75 |
--------------------------------------------------------------------------------
/backend/src/routes.js:
--------------------------------------------------------------------------------
1 | import { Router } from 'express';
2 |
3 | import Brute from 'express-brute';
4 | import BruteRedis from 'express-brute-redis';
5 |
6 | import UserController from './app/Controllers/UserController';
7 | import StudentController from './app/Controllers/StudentController';
8 | import PlanController from './app/Controllers/PlanController';
9 | import RegistrationController from './app/Controllers/RegistrationController';
10 | import CheckinController from './app/Controllers/CheckinController';
11 | import StudentOrderController from './app/Controllers/StudentOrderController';
12 | import authMiddleware from './app/middlewares/auth';
13 | import GymOrderController from './app/Controllers/GymOrderController';
14 | import SessionController from './app/Controllers/SessionController';
15 |
16 | const routes = new Router();
17 |
18 | const bruteStore = new BruteRedis({
19 | host: process.env.REDIS_HOST,
20 | port: process.env.REDOS_PORT,
21 | });
22 |
23 | const bruteForce = new Brute(bruteStore);
24 |
25 | routes.post('/session', bruteForce.prevent, UserController.store);
26 |
27 | routes.put('/student/:id', StudentController.update);
28 | routes.post('/students/:id/checkins', CheckinController.store);
29 | routes.get('/students/:id/checkins', CheckinController.index);
30 | routes.post('/students/:id/help-orders', StudentOrderController.store);
31 | routes.get('/students/:id/help-orders', StudentOrderController.index);
32 | routes.post('/login', SessionController.store);
33 |
34 | routes.use(authMiddleware);
35 | routes.post('/student', StudentController.store);
36 | routes.get('/students', StudentController.show);
37 | routes.get('/student', StudentController.index);
38 | routes.put('/student/:id', StudentController.update);
39 | routes.delete('/student/:id', StudentController.delete);
40 |
41 | routes.get('/plan', PlanController.index);
42 | routes.post('/plan', PlanController.store);
43 | routes.put('/plan/:planId', PlanController.update);
44 | routes.delete('/plan/:planId', PlanController.delete);
45 |
46 | routes.post('/registration', RegistrationController.store);
47 | routes.get('/registration', RegistrationController.index);
48 | routes.delete('/registration/:regisId', RegistrationController.delete);
49 | routes.put('/registration/:regisId', RegistrationController.update);
50 |
51 | routes.post('/help-orders/:id/answer', GymOrderController.store);
52 | routes.get('/help-orders', GymOrderController.index);
53 | routes.delete('/help-orders/:id', GymOrderController.delete);
54 |
55 | export default routes;
56 |
--------------------------------------------------------------------------------
/backend/src/app/Controllers/RegistrationController.js:
--------------------------------------------------------------------------------
1 | import * as Yup from 'yup';
2 | import { addDays } from 'date-fns';
3 | import Registration from '../models/Registration';
4 | import Student from '../models/Student';
5 | import Plan from '../models/Plan';
6 | import RegistrationMail from '../jobs/RegistrationMail';
7 | import Queue from '../../lib/Queue';
8 |
9 | class RegistrationController {
10 | async store(req, res) {
11 | const schema = Yup.object().shape({
12 | student_id: Yup.string().required(),
13 | plan_id: Yup.string().required(),
14 | });
15 | if (!(await schema.isValid(req.body))) {
16 | return res.status(400).json({ error: 'Erro na validação' });
17 | }
18 | const { student_id, plan_id } = req.body;
19 |
20 | const plan = await Plan.findByPk(plan_id);
21 |
22 | const student = await Student.findByPk(student_id);
23 | if (!student) {
24 | return res.status(400).json({ error: 'Id invalido' });
25 | }
26 |
27 | const checkRegistration = await Registration.findOne({
28 | where: { student_id },
29 | });
30 | if (checkRegistration) {
31 | return res.status(400).json({ error: 'O aluno já esta matriculado' });
32 | }
33 |
34 | const registration = await Registration.create({
35 | ...req.body,
36 | start_date: new Date(),
37 | end_date: addDays(new Date(), plan.duration),
38 | price: plan.price,
39 | });
40 |
41 | await Queue.add(RegistrationMail.key, {
42 | plan,
43 | student,
44 | });
45 |
46 | return res.json(registration);
47 | }
48 |
49 | async index(req, res) {
50 | const registrations = await Registration.findAll({
51 | attributes: ['id', 'price', 'start_date', 'end_date'],
52 |
53 | include: [
54 | { model: Student, as: 'student', attributes: ['id', 'name', 'email'] },
55 | { model: Plan, as: 'plan', attributes: ['id', 'title'] },
56 | ],
57 | });
58 |
59 | return res.json(registrations);
60 | }
61 |
62 | async delete(req, res) {
63 | await Registration.destroy({ where: { id: req.params.regisId } });
64 | return res.json();
65 | }
66 |
67 | async update(req, res) {
68 | const student = await Registration.findByPk(req.params.regisId);
69 | const { plan_id } = req.body;
70 | const plan = await Plan.findByPk(plan_id);
71 |
72 | const showUpdate = await student.update({
73 | plan_id,
74 | end_date: addDays(student.start_date, plan.duration),
75 | price: plan.price,
76 | });
77 |
78 | return res.json(showUpdate);
79 | }
80 | }
81 | export default new RegistrationController();
82 |
--------------------------------------------------------------------------------
/frontend/src/Pages/Plans/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Container = styled.div`
4 | width: 100%;
5 | max-width: 1500px;
6 | margin: 20px auto;
7 | height: 468px;
8 | display: flex;
9 | padding: 15px;
10 | `
11 |
12 | export const Header = styled.header`
13 | background: rgba(0, 0, 0, 0.5);
14 | border: 1px solid #777;
15 | margin-right: 25px;
16 | padding: 20px 50px;
17 | width: 400px;
18 | height: 150px;
19 | border-top-left-radius: 50px;
20 | border-bottom-right-radius: 50px;
21 |
22 | > strong {
23 | font-size: 40px;
24 | color: #fff;
25 | align-self: center;
26 | }
27 |
28 | div {
29 | button {
30 | margin-top: 25px;
31 | width: 100%;
32 | background: #dc143c;
33 | border: 0;
34 | padding: 5px;
35 | color: #fff;
36 | font-weight: bold;
37 |
38 | > svg {
39 | margin-right: 5px;
40 | color: #fff;
41 | }
42 | }
43 |
44 | > svg {
45 | color: #777;
46 | font-size: 20px;
47 | position: absolute;
48 | margin-top: 5px;
49 | margin-left: 140px;
50 | }
51 |
52 | input {
53 | width: 200px;
54 | margin-left: 5px;
55 | padding: 5px;
56 | border-radius: 4px;
57 | border: 1px solid #dbdbda;
58 |
59 | &::placeholder {
60 | padding-left: 10px;
61 | }
62 | }
63 | }
64 | `
65 |
66 | export const Content = styled.div`
67 | display: ${props => (props.search ? 'none' : 'relative')};
68 | width: 1000px;
69 | margin: 0 auto;
70 | overflow: scroll;
71 |
72 | ::-webkit-scrollbar {
73 | width: 10px;
74 | height: 5px;
75 | }
76 |
77 | ::-webkit-scrollbar-thumb {
78 | background: rgba(207, 0, 0, 0.83);
79 | border-radius: 10px;
80 | }
81 |
82 | table {
83 | width: 100%;
84 | border-collapse: collapse;
85 | thead th {
86 | width: 80px;
87 | color: #fff;
88 | text-align: left;
89 | padding: 12px;
90 | }
91 | tbody {
92 | tr {
93 | color: #fff;
94 | border-bottom: 1px solid #4f4f4f;
95 | background: rgba(100, 100, 100, 0.5);
96 |
97 | &:nth-child(even) {
98 | background: rgba(0, 0, 0, 0.08);
99 | }
100 | td {
101 | padding: 12px;
102 |
103 | img {
104 | height: 35px;
105 | border-radius: 50%;
106 | }
107 |
108 | button {
109 | background: 0;
110 | border: 0;
111 | }
112 | }
113 | }
114 | }
115 | }
116 | `
117 |
--------------------------------------------------------------------------------
/frontend/src/Pages/Registrations/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Container = styled.div`
4 | width: 100%;
5 | max-width: 1500px;
6 | margin: 20px auto;
7 | height: 468px;
8 | display: flex;
9 | padding: 15px;
10 | `
11 |
12 | export const Header = styled.header`
13 | background: rgba(0, 0, 0, 0.5);
14 | border: 1px solid #777;
15 | margin-right: 25px;
16 | padding: 20px 50px;
17 | width: 400px;
18 | height: 150px;
19 | border-top-left-radius: 50px;
20 | border-bottom-right-radius: 50px;
21 |
22 | > strong {
23 | font-size: 40px;
24 | color: #fff;
25 | align-self: center;
26 | }
27 |
28 | div {
29 | button {
30 | width: 100%;
31 | margin-top: 25px;
32 | background: #dc143c;
33 | border: 0;
34 | padding: 5px;
35 | color: #fff;
36 | font-weight: bold;
37 |
38 | > svg {
39 | margin-right: 5px;
40 | color: #fff;
41 | }
42 | }
43 |
44 | > svg {
45 | color: #777;
46 | font-size: 20px;
47 | position: absolute;
48 | margin-top: 5px;
49 | margin-left: 140px;
50 | }
51 | }
52 | }
53 | `
54 |
55 | export const Content = styled.div`
56 | display: ${props => (props.search ? 'none' : 'relative')};
57 | width: 1000px;
58 | margin: 0 auto;
59 |
60 | table {
61 | width: 100%;
62 | border-collapse: collapse;
63 | thead th {
64 | width: 80px;
65 | color: #fff;
66 | text-align: left;
67 | padding: 12px;
68 | }
69 | tbody {
70 | tr {
71 | color: #fff;
72 | border-bottom: 1px solid #4f4f4f;
73 | background: rgba(100, 100, 100, 0.5);
74 |
75 | &:nth-child(even) {
76 | background: rgba(0, 0, 0, 0.08);
77 | }
78 | td {
79 | padding: 12px;
80 | img {
81 | height: 35px;
82 | border-radius: 50%;
83 | }
84 |
85 | button {
86 | background: 0;
87 | border: 0;
88 | }
89 | }
90 | }
91 | }
92 | }
93 | `
94 |
95 | export const SearchContent = styled.div`
96 | display: ${props => (props.search ? 'relative' : 'none')};
97 | width: 100%;
98 | max-width: 800px;
99 | height: 400px;
100 | background: #fff;
101 | margin-bottom: 15px;
102 | border-radius: 5px;
103 |
104 | table {
105 | width: 100%;
106 | thead th {
107 | width: 80px;
108 | color: #999;
109 | text-align: left;
110 | padding: 12px;
111 | }
112 | tbody td {
113 | padding: 12px;
114 | }
115 |
116 | button {
117 | background: 0;
118 | border: 0;
119 | }
120 | }
121 | `
122 |
--------------------------------------------------------------------------------
/frontend/src/Pages/Registrations/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { FaPlus, FaTrashAlt } from 'react-icons/fa'
3 | import { format, parseISO } from 'date-fns'
4 | import api from '../../services/api'
5 | import { Container, Header, Content } from './styles'
6 |
7 | import ModalRegister from '../../components/Modals/Registration/Register'
8 | import ModalDelete from '../../components/Modals/Registration/Delete'
9 |
10 | export default function Registrations() {
11 | const [registrations, setRegistrations] = useState([])
12 | const [registration, setRegistration] = useState([])
13 |
14 | const [open, setOpen] = useState(false)
15 | const [modelDelete, setModalDelete] = useState(false)
16 |
17 | useEffect(() => {
18 | async function loadRegistrations() {
19 | const response = await api.get('registration')
20 |
21 | const regis = response.data.map(data => {
22 | return {
23 | id: data.id,
24 | name: data.student.name,
25 | plan: data.plan.title,
26 | startDate: format(parseISO(data.start_date), 'dd/MM/yyyy'),
27 | endDate: format(parseISO(data.end_date), 'dd/MM/yyyy'),
28 | }
29 | })
30 | setRegistrations(regis)
31 | }
32 | loadRegistrations()
33 | }, [])
34 | return (
35 |
36 | setOpen(false)} />
37 | setModalDelete(false)}
40 | regId={registration}
41 | />
42 |
52 |
53 |
54 |
55 |
56 | | Aluno |
57 | Plano |
58 | Início |
59 | Término |
60 | Remover |
61 |
62 |
63 |
64 | {registrations.map(reg => (
65 |
66 | | {reg.name} |
67 | {reg.plan} |
68 | {reg.startDate} |
69 | {reg.endDate} |
70 |
71 |
80 | |
81 |
82 | ))}
83 |
84 |
85 |
86 |
87 | )
88 | }
89 |
--------------------------------------------------------------------------------
/frontend/src/Pages/Students/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | import searchIcon from '../../assets/searchicon.png'
4 |
5 | export const Container = styled.div`
6 | width: 100%;
7 | max-width: 1500px;
8 | margin: 20px auto;
9 | height: 468px;
10 | display: flex;
11 | padding: 15px;
12 | `
13 |
14 | export const Header = styled.header`
15 | background: rgba(0, 0, 0, 0.5);
16 | border: 1px solid #777;
17 | margin-right: 25px;
18 | padding: 20px 50px;
19 | width: 400px;
20 | height: 240px;
21 | border-top-left-radius: 50px;
22 | border-bottom-right-radius: 50px;
23 |
24 | div {
25 | width: 250px;
26 |
27 | h1 {
28 | margin-bottom: 35px;
29 | color: #fff;
30 | font-size: 35px;
31 | }
32 |
33 | strong {
34 | color: #fff;
35 | font-weight: bold;
36 | display: block;
37 | }
38 | input {
39 | background-image: url(${searchIcon});
40 | background-position: 10px 5px;
41 | background-repeat: no-repeat;
42 | background-size: 25px;
43 | padding: 10px 10px 10px 45px;
44 | border: 0;
45 | border-radius: 5px;
46 | width: 50px;
47 | -webkit-transition: width 1s ease-in-out;
48 | transition: width 0.4s ease-in-out;
49 |
50 | &::placeholder {
51 | padding-left: 8px;
52 | }
53 |
54 | &:focus {
55 | width: 100%;
56 | }
57 | }
58 | }
59 |
60 | button {
61 | margin-top: 20px;
62 | background: #dc143c;
63 | border: 0;
64 | padding: 5px;
65 | color: #fff;
66 | font-weight: bold;
67 | width: 100%;
68 |
69 | display: flex;
70 | justify-content: center;
71 |
72 | > svg {
73 | margin-right: 5px;
74 | color: #fff;
75 | }
76 | }
77 | `
78 | export const SearchStudent = styled.div`
79 | position: absolute;
80 | width: 310px;
81 | height: 150px;
82 | background: white;
83 | border: 1px solid black;
84 | `
85 |
86 | export const Content = styled.div`
87 | display: ${props => (props.search ? 'none' : 'relative')};
88 | width: 1000px;
89 | height: 400px;
90 | margin: 0 auto;
91 | overflow: auto;
92 | ::-webkit-scrollbar {
93 | width: 10px;
94 | height: 5px;
95 | }
96 |
97 | ::-webkit-scrollbar-thumb {
98 | background: rgba(207, 0, 0, 0.83);
99 | border-radius: 10px;
100 | }
101 |
102 | table {
103 | width: 100%;
104 | border-collapse: collapse;
105 |
106 | thead {
107 | th {
108 | width: 80px;
109 | color: #fff;
110 | text-align: left;
111 | padding: 12px;
112 | }
113 | }
114 |
115 | tbody {
116 | tr {
117 | color: #fff;
118 | border-bottom: 1px solid #4f4f4f;
119 | background: rgba(100, 100, 100, 0.5);
120 |
121 | &:nth-child(even) {
122 | background: rgba(0, 0, 0, 0.08);
123 | }
124 |
125 | td {
126 | padding: 12px;
127 |
128 | img {
129 | height: 40px;
130 | border-radius: 50%;
131 | }
132 |
133 | button {
134 | background: 0;
135 | border: 0;
136 | }
137 | }
138 | }
139 | }
140 | }
141 | `
142 |
--------------------------------------------------------------------------------
/frontend/src/Pages/Plans/index.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo, useState } from 'react'
2 | import { FaPlus, FaTrashAlt, FaEdit } from 'react-icons/fa'
3 |
4 | import api from '../../services/api'
5 | import { Container, Header, Content } from './styles'
6 | import { PriceFormat } from '../../util/util'
7 | import ModalPlanRegister from '../../components/Modals/Plan/Register'
8 | import ModalPlanDelete from '../../components/Modals/Plan/Delete'
9 | import ModalPlanUpdate from '../../components/Modals/Plan/Update'
10 |
11 | export default function Plans() {
12 | const [plans, setPlans] = useState([])
13 | const [plan, setPlan] = useState({})
14 | const [planId, setPlanId] = useState([])
15 |
16 | const [open, setOpen] = useState(false)
17 | const [modalDelete, setModalDelete] = useState(false)
18 | const [modalUpdate, setModalUpdate] = useState(false)
19 |
20 | useMemo(async () => {
21 | const response = await api.get('/plan')
22 |
23 | const data = response.data.map(plano => {
24 | return {
25 | ...plano,
26 | priceFormatted: PriceFormat(plano.price),
27 | }
28 | })
29 | setPlans(data)
30 | }, [setPlans])
31 |
32 | return (
33 |
34 | setOpen(false)} />
35 | setModalDelete(false)}
38 | planId={planId}
39 | />
40 | setModalUpdate(false)}
44 | />
45 |
55 |
56 |
57 |
58 |
59 | | Nome |
60 | Preço |
61 | Duração |
62 | Editar |
63 | Remover |
64 |
65 |
66 |
67 | {plans.map(Plan => (
68 |
69 | |
70 | {Plan.title}
71 | |
72 | {Plan.priceFormatted} |
73 | {Plan.duration} Dias |
74 |
75 |
84 | |
85 |
86 |
95 | |
96 |
97 | ))}
98 |
99 |
100 |
101 |
102 | )
103 | }
104 |
--------------------------------------------------------------------------------
/frontend/src/Pages/Students/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useMemo } from 'react'
2 | import { FaTrashAlt, FaEdit } from 'react-icons/fa'
3 | import { IoMdPersonAdd } from 'react-icons/io'
4 |
5 | import { Container, Header, Content } from './styles'
6 |
7 | import ModalRegister from '../../components/Modals/Student/Register'
8 | import ModalDelete from '../../components/Modals/Student/Delete'
9 | import ModalUpdate from '../../components/Modals/Student/Update'
10 |
11 | import api from '../../services/api'
12 |
13 | export default function Students() {
14 | const [students, setStudents] = useState([])
15 | const [studentId, setStudentId] = useState([])
16 | const [student, setStudent] = useState([])
17 | const [searchStudent, setSearchStudent] = useState('')
18 | const [open, setOpen] = useState(false)
19 | const [openDelete, setOpenDelete] = useState(false)
20 | const [openUpdate, setOpenUpdate] = useState(false)
21 |
22 | useMemo(async () => {
23 | const response =
24 | searchStudent === ''
25 | ? await api.get(`students`)
26 | : await api.get(`student?name=${searchStudent}`)
27 | setStudents(response.data)
28 | }, [searchStudent, setStudents])
29 |
30 | return (
31 |
32 | setOpen(false)} />
33 | setOpenDelete(false)}
36 | studentId={studentId}
37 | />
38 | setOpenUpdate(false)}
41 | student={student}
42 | />
43 |
60 |
61 |
62 |
63 |
64 | | Nome |
65 | E-mail |
66 | Idade |
67 | Peso |
68 | Altura |
69 | Editar |
70 | Remover |
71 |
72 |
73 |
74 | {students.map(Student => (
75 |
76 | |
77 | {Student.name}
78 | |
79 | {Student.email} |
80 | {Student.age} anos |
81 | {Student.weight} Kg |
82 | {Student.height}m |
83 |
84 |
93 | |
94 |
95 |
104 | |
105 |
106 | ))}
107 |
108 |
109 |
110 |
111 | )
112 | }
113 |
--------------------------------------------------------------------------------