├── frontend ├── README.md ├── src │ ├── store │ │ ├── modules │ │ │ ├── user │ │ │ │ ├── actions.js │ │ │ │ ├── sagas.js │ │ │ │ └── reducer.js │ │ │ ├── rootReducer.js │ │ │ ├── rootSaga.js │ │ │ └── auth │ │ │ │ ├── actions.js │ │ │ │ ├── reducer.js │ │ │ │ └── sagas.js │ │ ├── persistReducers.js │ │ ├── createStore.js │ │ └── index.js │ ├── assets │ │ ├── logo-header.png │ │ ├── search.svg │ │ └── logo-header.svg │ ├── services │ │ ├── history.js │ │ └── api.js │ ├── index.js │ ├── pages │ │ ├── _layouts │ │ │ ├── default │ │ │ │ ├── styles.js │ │ │ │ └── index.js │ │ │ └── auth │ │ │ │ ├── index.js │ │ │ │ └── styles.js │ │ ├── SignIn │ │ │ └── index.js │ │ ├── HelpOrders │ │ │ ├── styles.js │ │ │ └── index.js │ │ ├── Plans │ │ │ ├── ManagePlans │ │ │ │ └── styles.js │ │ │ ├── styles.js │ │ │ └── index.js │ │ ├── Students │ │ │ ├── ManageStudent │ │ │ │ └── styles.js │ │ │ └── styles.js │ │ └── Memberships │ │ │ ├── ManageMembership │ │ │ └── styles.js │ │ │ └── styles.js │ ├── components │ │ ├── Loading │ │ │ ├── styles.js │ │ │ └── index.js │ │ ├── Header │ │ │ ├── Menu │ │ │ │ ├── index.js │ │ │ │ └── styles.js │ │ │ ├── styles.js │ │ │ └── index.js │ │ ├── Pagination │ │ │ ├── styles.js │ │ │ └── index.js │ │ ├── HeightInput │ │ │ └── index.js │ │ └── Modal │ │ │ ├── index.js │ │ │ └── styles.js │ ├── styles │ │ ├── colors.js │ │ └── global.js │ ├── helpers │ │ ├── format.js │ │ ├── LinkWrapper │ │ │ └── index.js │ │ └── hooks │ │ │ └── useComponentVisible.js │ ├── config │ │ └── ReactotronConfig.js │ ├── App.js │ └── routes │ │ ├── Route.js │ │ └── index.js ├── .prettierrc ├── public │ ├── favicon.ico │ └── index.html ├── jsconfig.json ├── .editorconfig ├── config-overrides.js ├── .gitignore ├── .eslintrc.js └── package.json ├── mobile ├── .watchmanconfig ├── .gitattributes ├── app.json ├── .prettierrc ├── src │ ├── assets │ │ ├── logo.png │ │ ├── 8-layers.png │ │ └── logo-header.png │ ├── components │ │ ├── Background │ │ │ ├── index.js │ │ │ └── BackgroundSignIn │ │ │ │ └── index.js │ │ ├── Input │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── Button │ │ │ ├── styles.js │ │ │ └── index.js │ │ ├── Header │ │ │ ├── styles.js │ │ │ └── index.js │ │ └── Order │ │ │ ├── styles.js │ │ │ └── index.js │ ├── store │ │ ├── modules │ │ │ ├── rootReducer.js │ │ │ ├── rootSaga.js │ │ │ └── student │ │ │ │ ├── actions.js │ │ │ │ ├── reducer.js │ │ │ │ └── sagas.js │ │ ├── createStore.js │ │ ├── persistReducers.js │ │ └── index.js │ ├── services │ │ └── api.js │ ├── index.js │ ├── pages │ │ ├── HelpOrdersPages │ │ │ ├── HelpOrders │ │ │ │ └── styles.js │ │ │ ├── NewOrder │ │ │ │ ├── styles.js │ │ │ │ └── index.js │ │ │ └── Answer │ │ │ │ ├── styles.js │ │ │ │ └── index.js │ │ ├── SignIn │ │ │ ├── styles.js │ │ │ └── index.js │ │ ├── CheckInsPage │ │ │ └── Checkins │ │ │ │ └── styles.js │ │ └── ProfilePage │ │ │ └── Profile │ │ │ ├── styles.js │ │ │ └── index.js │ ├── config │ │ └── ReactotronConfig.js │ ├── App.js │ └── routes.js ├── android │ ├── app │ │ ├── debug.keystore │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── values │ │ │ │ │ │ ├── strings.xml │ │ │ │ │ │ └── styles.xml │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ └── mipmap-xxxhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── java │ │ │ │ │ └── com │ │ │ │ │ │ └── mobile │ │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ │ └── MainApplication.java │ │ │ │ └── AndroidManifest.xml │ │ │ └── debug │ │ │ │ └── AndroidManifest.xml │ │ ├── proguard-rules.pro │ │ ├── build_defs.bzl │ │ └── BUCK │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ ├── gradle.properties │ ├── build.gradle │ └── gradlew.bat ├── ios │ ├── mobile │ │ ├── Images.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── AppDelegate.h │ │ ├── main.m │ │ ├── AppDelegate.m │ │ ├── Info.plist │ │ └── Base.lproj │ │ │ └── LaunchScreen.xib │ ├── mobileTests │ │ ├── Info.plist │ │ └── mobileTests.m │ ├── mobile-tvOSTests │ │ └── Info.plist │ ├── mobile-tvOS │ │ └── Info.plist │ └── Podfile ├── jsconfig.json ├── .buckconfig ├── .editorconfig ├── babel.config.js ├── index.js ├── __tests__ │ └── App-test.js ├── metro.config.js ├── .gitignore ├── .eslintrc.js ├── package.json └── .flowconfig ├── backend ├── src │ ├── server.js │ ├── queue.js │ ├── assets │ │ └── logo.png │ ├── bootstrap.js │ ├── config │ │ ├── auth.js │ │ ├── redis.js │ │ ├── mail.js │ │ └── database.js │ ├── app │ │ ├── views │ │ │ └── emails │ │ │ │ ├── partials │ │ │ │ ├── header.hbs │ │ │ │ └── footer.hbs │ │ │ │ ├── layouts │ │ │ │ └── default.hbs │ │ │ │ ├── help-order-answer.hbs │ │ │ │ └── confirmation.hbs │ │ ├── models │ │ │ ├── Plan.js │ │ │ ├── Checkin.js │ │ │ ├── Student.js │ │ │ ├── Order.js │ │ │ ├── User.js │ │ │ └── Membership.js │ │ ├── jobs │ │ │ ├── HelpOrderAnswerMail.js │ │ │ └── ConfirmationMail.js │ │ ├── middlewares │ │ │ └── auth.js │ │ └── controllers │ │ │ ├── SessionController.js │ │ │ ├── AnswerController.js │ │ │ ├── OrderController.js │ │ │ ├── UserController.js │ │ │ ├── PlanController.js │ │ │ ├── CheckinController.js │ │ │ └── StudentController.js │ ├── database │ │ ├── seeds │ │ │ └── 20191016115833-admin-user.js │ │ ├── index.js │ │ └── migrations │ │ │ ├── 20191019155243-create-checkins.js │ │ │ ├── 20191016121858-create-users.js │ │ │ ├── 20191018151913-plan-management.js │ │ │ ├── 20191019173908-create-help-orders.js │ │ │ ├── 20191016133943-create-students.js │ │ │ └── 20191018173726-membership-management.js │ ├── lib │ │ ├── Queue.js │ │ └── Mail.js │ ├── app.js │ └── routes.js ├── .prettierrc ├── nodemon.json ├── .editorconfig ├── .env.test ├── .env.example ├── .sequelizerc ├── __tests__ │ ├── util │ │ └── truncate.js │ ├── getToken.js │ ├── factories.js │ └── integration │ │ ├── session.test.js │ │ └── answer.test.js ├── .vscode │ └── launch.json ├── .gitignore ├── .eslintrc.js └── package.json ├── renovate.json └── LICENSE /frontend/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mobile/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /frontend/src/store/modules/user/actions.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/store/modules/user/sagas.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mobile/.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /backend/src/server.js: -------------------------------------------------------------------------------- 1 | import app from './app'; 2 | 3 | app.listen(3003); 4 | -------------------------------------------------------------------------------- /mobile/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobile", 3 | "displayName": "mobile" 4 | } -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } -------------------------------------------------------------------------------- /backend/src/queue.js: -------------------------------------------------------------------------------- 1 | import Queue from './lib/Queue'; 2 | 3 | Queue.processQueue(); 4 | -------------------------------------------------------------------------------- /mobile/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | } 5 | -------------------------------------------------------------------------------- /backend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/backend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /mobile/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/src/assets/logo.png -------------------------------------------------------------------------------- /mobile/src/assets/8-layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/src/assets/8-layers.png -------------------------------------------------------------------------------- /mobile/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/debug.keystore -------------------------------------------------------------------------------- /mobile/src/assets/logo-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/src/assets/logo-header.png -------------------------------------------------------------------------------- /backend/src/bootstrap.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ 2 | path: process.env.NODE_ENV === 'test' ? '.env.test' : '.env', 3 | }); 4 | -------------------------------------------------------------------------------- /frontend/src/assets/logo-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/frontend/src/assets/logo-header.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | mobile 3 | 4 | -------------------------------------------------------------------------------- /mobile/ios/mobile/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /backend/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "execMap": { 3 | "js": "node -r sucrase/register" 4 | }, 5 | "ignore": [ 6 | "__tests__" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/config/auth.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | export default { 4 | secret: process.env.APP_SECRET, 5 | expiresIn: '7d', 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "paths": { 5 | "~/*": ["*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /mobile/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "paths": { 5 | "~/*": ["*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /mobile/.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /mobile/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const api = axios.create({ 4 | baseURL: 'http://localhost:3003', 5 | }); 6 | 7 | export default api; 8 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/src/components/Background/index.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export default styled.View` 4 | flex: 1; 5 | background: #f5f5f5; 6 | `; 7 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /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/pages/_layouts/default/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Wrapper = styled.div` 4 | height: 100%; 5 | background: #f5f5f5; 6 | `; 7 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /mobile/src/components/Background/BackgroundSignIn/index.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export default styled.View` 4 | flex: 1; 5 | background: #fff; 6 | `; 7 | -------------------------------------------------------------------------------- /mobile/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /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 9 | end_of_line = lf -------------------------------------------------------------------------------- /mobile/src/store/modules/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import student from './student/reducer'; 4 | 5 | export default combineReducers({ 6 | student, 7 | }); 8 | -------------------------------------------------------------------------------- /mobile/src/store/modules/rootSaga.js: -------------------------------------------------------------------------------- 1 | import { all } from 'redux-saga/effects'; 2 | 3 | import student from './student/sagas'; 4 | 5 | export default function* rootSaga() { 6 | return yield all([student]); 7 | } 8 | -------------------------------------------------------------------------------- /mobile/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'mobile' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | -------------------------------------------------------------------------------- /backend/src/app/views/emails/partials/header.hbs: -------------------------------------------------------------------------------- 1 | GymPoint Logo 2 | -------------------------------------------------------------------------------- /frontend/src/store/modules/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import auth from './auth/reducer'; 4 | import user from './user/reducer'; 5 | 6 | export default combineReducers({ 7 | auth, 8 | user, 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/src/store/modules/rootSaga.js: -------------------------------------------------------------------------------- 1 | import { all } from 'redux-saga/effects'; 2 | 3 | import auth from './auth/sagas'; 4 | import user from './user/sagas'; 5 | 6 | export default function* rootSaga() { 7 | return yield all([auth, user]); 8 | } 9 | -------------------------------------------------------------------------------- /backend/.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 9 | end_of_line = lf 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /frontend/src/components/Loading/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | height: 100%; 5 | padding-top: 72px; 6 | 7 | @media (min-width: 768px) { 8 | padding-left: 50%; 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /mobile/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | plugins: [ 4 | [ 5 | 'babel-plugin-root-import', 6 | { 7 | rootPathSuffix: 'src', 8 | }, 9 | ], 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /backend/src/app/views/emails/partials/footer.hbs: -------------------------------------------------------------------------------- 1 |

⌚️ Horario de funcionamento:

2 | 3 | Segunda a sexta: 06:00 - 22:00
4 | Sabados e domingos: 08:00 - 18:00 5 |
6 |
7 | Equipe GymPoint 💪💪 8 | -------------------------------------------------------------------------------- /frontend/config-overrides.js: -------------------------------------------------------------------------------- 1 | const { addBabelPlugin, override } = require('customize-cra'); 2 | 3 | module.exports = override( 4 | addBabelPlugin([ 5 | 'babel-plugin-root-import', 6 | { 7 | rootPathSuffix: 'src', 8 | }, 9 | ]) 10 | ); 11 | -------------------------------------------------------------------------------- /mobile/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /frontend/src/styles/colors.js: -------------------------------------------------------------------------------- 1 | export default { 2 | primary: '#ee4d64', 3 | editButton: '#4d85ee', 4 | backButton: '#ccc', 5 | white: '#fff', 6 | border: '#ddd', 7 | lightBorder: '#eee', 8 | lightGray: '#999', 9 | gray: '#666', 10 | darkGray: '#444', 11 | }; 12 | -------------------------------------------------------------------------------- /mobile/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import { AppRegistry } from 'react-native'; 6 | import 'react-native-gesture-handler'; 7 | import App from './src'; 8 | import { name as appName } from './app.json'; 9 | 10 | AppRegistry.registerComponent(appName, () => App); 11 | -------------------------------------------------------------------------------- /frontend/src/helpers/format.js: -------------------------------------------------------------------------------- 1 | export const unFormat = target => 2 | target 3 | .toString() 4 | .replace(/[R$.]/g, '') 5 | .replace(',', '.'); 6 | 7 | export const { format: formatPrice } = new Intl.NumberFormat('pt-BR', { 8 | style: 'currency', 9 | currency: 'BRL', 10 | }); 11 | -------------------------------------------------------------------------------- /backend/src/app/views/emails/layouts/default.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{> header }} 3 | {{{ body }}} 4 | {{> footer }} 5 |
6 | -------------------------------------------------------------------------------- /backend/.env.test: -------------------------------------------------------------------------------- 1 | APP_URL=http://localhost:3003 2 | NODE_ENV=test 3 | 4 | # Auth 5 | 6 | APP_SECRET=secrettests 7 | 8 | # Database 9 | 10 | DB_DIALECT=sqlite 11 | 12 | # Redis 13 | 14 | REDIS_HOST=127.0.0.1 15 | REDIS_POST=6379 16 | 17 | # Mail 18 | 19 | MAIL_HOST= 20 | MAIL_PORT= 21 | MAIL_USER= 22 | MAIL_PASS= 23 | -------------------------------------------------------------------------------- /frontend/src/helpers/LinkWrapper/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | import colors from '~/styles/colors'; 5 | 6 | const LinkWrapper = props => { 7 | return ; 8 | }; 9 | export default LinkWrapper; 10 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /mobile/__tests__/App-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import 'react-native'; 6 | import React from 'react'; 7 | import App from '../App'; 8 | 9 | // Note: test renderer must be required after react-native. 10 | import renderer from 'react-test-renderer'; 11 | 12 | it('renders correctly', () => { 13 | renderer.create(); 14 | }); 15 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | APP_URL=http://localhost:3003 2 | NODE_ENV=development 3 | 4 | # Auth 5 | 6 | APP_SECRET= 7 | 8 | # Database 9 | 10 | DB_HOST= 11 | DB_USER= 12 | DB_PASS= 13 | DB_NAME= 14 | 15 | # Redis 16 | 17 | REDIS_HOST=127.0.0.1 18 | REDIS_POST=6379 19 | 20 | # Mail 21 | 22 | MAIL_HOST= 23 | MAIL_PORT= 24 | MAIL_USER= 25 | MAIL_PASS= 26 | 27 | -------------------------------------------------------------------------------- /backend/.sequelizerc: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | 3 | module.exports = { 4 | config: resolve(__dirname, 'src', 'config', 'database.js'), 5 | 'models-path': resolve(__dirname, 'src', 'app', 'models'), 6 | 'migrations-path': resolve(__dirname, 'src', 'database', 'migrations'), 7 | 'seeders-path': resolve(__dirname, 'src', 'database', 'seeds'), 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/config/mail.js: -------------------------------------------------------------------------------- 1 | require('dotenv/config'); 2 | 3 | export default { 4 | host: process.env.MAIL_HOST, 5 | port: process.env.MAIL_PORT, 6 | secure: false, 7 | auth: { 8 | user: process.env.MAIL_USER, 9 | pass: process.env.MAIL_PASS, 10 | }, 11 | default: { 12 | from: 'Equipe GymPoint ', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /mobile/src/store/createStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | 3 | export default (reducers, middlewares) => { 4 | const enhancer = __DEV__ 5 | ? compose(console.tron.createEnhancer(), applyMiddleware(...middlewares)) 6 | : applyMiddleware(...middlewares); 7 | 8 | return createStore(reducers, enhancer); 9 | }; 10 | -------------------------------------------------------------------------------- /backend/__tests__/util/truncate.js: -------------------------------------------------------------------------------- 1 | import database from '../../src/database'; 2 | 3 | export default function truncate() { 4 | return Promise.all( 5 | Object.keys(database.connection.models).map(key => { 6 | return database.connection.models[key].destroy({ 7 | truncate: true, 8 | force: true, 9 | }); 10 | }) 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /mobile/src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | /* 4 | url: { 5 | ios: localhost 6 | android: { 7 | android studio: 10.0.2.2, 8 | genymotion: 10.0.3.2, 9 | phone via usb: your ip 10 | } 11 | } 12 | */ 13 | 14 | const api = axios.create({ 15 | baseURL: 'http://10.0.2.2:3003', 16 | }); 17 | 18 | export default api; 19 | -------------------------------------------------------------------------------- /backend/src/app/views/emails/help-order-answer.hbs: -------------------------------------------------------------------------------- 1 | Olá, {{ student }} 2 | 3 |

Obrigado por entrar em contado, confira os detalhes de seu pedido abaixo:

4 |

5 | Sua pergunta: {{ question }}
6 | Resposta do instrutor: {{ answer }}

7 | Se ainda restarem duvidas, favor criar um novo pedido. 8 |

9 | -------------------------------------------------------------------------------- /mobile/metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /mobile/src/components/Input/index.js: -------------------------------------------------------------------------------- 1 | import React, {forwardRef} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import {Container, TInput} from './styles'; 5 | 6 | const Input = ({style, ...rest}, ref) => { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default forwardRef(Input); 15 | -------------------------------------------------------------------------------- /frontend/src/store/persistReducers.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: 'gympoint', 8 | storage, 9 | whitelist: ['auth', 'user'], 10 | }, 11 | reducers 12 | ); 13 | 14 | return persistedReducer; 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/src/pages/_layouts/auth/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { Wrapper, Content } from './styles'; 5 | 6 | export default function AuthLayout({ children }) { 7 | return ( 8 | 9 | {children} 10 | 11 | ); 12 | } 13 | 14 | AuthLayout.propTypes = { 15 | children: PropTypes.element.isRequired, 16 | }; 17 | -------------------------------------------------------------------------------- /mobile/src/store/persistReducers.js: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-community/async-storage'; 2 | import { persistReducer } from 'redux-persist'; 3 | 4 | export default reducers => { 5 | const persistedReducer = persistReducer( 6 | { 7 | key: 'gympoint', 8 | storage: AsyncStorage, 9 | whitelist: ['student'], 10 | }, 11 | reducers 12 | ); 13 | 14 | return persistedReducer; 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /mobile/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /frontend/src/store/modules/user/reducer.js: -------------------------------------------------------------------------------- 1 | import produce from 'immer'; 2 | 3 | const initial_state = { 4 | profile: null, 5 | }; 6 | 7 | export default function user(state = initial_state, action) { 8 | switch (action.type) { 9 | case '@auth/SIGN_IN_SUCCESS': 10 | return produce(state, draft => { 11 | draft.profile = action.payload.user; 12 | }); 13 | default: 14 | return state; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /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 | duration: Sequelize.INTEGER, 9 | price: Sequelize.DOUBLE, 10 | }, 11 | { 12 | sequelize, 13 | } 14 | ); 15 | 16 | return this; 17 | } 18 | } 19 | 20 | export default Plan; 21 | -------------------------------------------------------------------------------- /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 | 12 | return this; 13 | } 14 | 15 | static associate(models) { 16 | this.belongsTo(models.Student, { foreignKey: 'student_id', as: 'student' }); 17 | } 18 | } 19 | 20 | export default Checkin; 21 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Challenge 9 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /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/store/createStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | 3 | export default (reducers, middlewares) => { 4 | const enhancer = 5 | process.env.NODE_ENV === 'development' 6 | ? compose( 7 | console.tron.createEnhancer(), 8 | applyMiddleware(...middlewares) 9 | ) 10 | : applyMiddleware(...middlewares); 11 | 12 | return createStore(reducers, enhancer); 13 | }; 14 | -------------------------------------------------------------------------------- /mobile/src/components/Button/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | import { RectButton } from 'react-native-gesture-handler'; 3 | 4 | export const Container = styled(RectButton)` 5 | height: 45px; 6 | background: #ee4e62; 7 | border-radius: 4px; 8 | align-items: center; 9 | justify-content: center; 10 | `; 11 | 12 | export const Text = styled.Text` 13 | color: #fff; 14 | font-weight: bold 700; 15 | font-size: 16px; 16 | `; 17 | -------------------------------------------------------------------------------- /backend/src/config/database.js: -------------------------------------------------------------------------------- 1 | require('../bootstrap'); 2 | 3 | module.exports = { 4 | dialect: process.env.DB_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 | storage: './__tests__/database.sqlite', 10 | logging: false, 11 | define: { 12 | timestamps: true, 13 | underscored: true, 14 | underscoredAll: true, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /backend/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "name": "Launch Program", 11 | "restart": true, 12 | "protocol": "inspector" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /backend/__tests__/getToken.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | 3 | import app from '../src/app'; 4 | import factory from './factories'; 5 | 6 | const getToken = async () => { 7 | const user = await factory.attrs('User'); 8 | 9 | await request(app) 10 | .post('/users') 11 | .send(user); 12 | 13 | const response = await request(app) 14 | .post('/sessions') 15 | .send(user); 16 | 17 | return response; 18 | }; 19 | 20 | export default getToken; 21 | -------------------------------------------------------------------------------- /frontend/src/pages/_layouts/default/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Header from '~/components/Header'; 5 | 6 | import { Wrapper } from './styles'; 7 | 8 | export default function DefaultLayout({ children }) { 9 | return ( 10 | 11 |
12 | {children} 13 | 14 | ); 15 | } 16 | 17 | DefaultLayout.propTypes = { 18 | children: PropTypes.element.isRequired, 19 | }; 20 | -------------------------------------------------------------------------------- /mobile/ios/mobile/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (nonatomic, strong) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /__tests__/coverage/* 10 | /__tests__/database.sqlite 11 | 12 | # production 13 | /dist 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /mobile/ios/mobile/main.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /mobile/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /mobile/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PersistGate } from 'redux-persist/integration/react'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import './config/ReactotronConfig'; 6 | 7 | import { store, persistor } from './store'; 8 | import App from './App'; 9 | 10 | export default function Index() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /mobile/src/components/Input/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Container = styled.View` 4 | padding: 0 10px; 5 | height: 45px; 6 | background: #fff; 7 | border-radius: 4px; 8 | flex-direction: row; 9 | align-items: center; 10 | border: 1px solid #ddd; 11 | `; 12 | 13 | export const TInput = styled.TextInput.attrs({ 14 | placeholderTextColor: '#a3a3a3', 15 | })` 16 | flex: 1; 17 | font-size: 16px; 18 | margin-left: 10px; 19 | color: #333; 20 | `; 21 | -------------------------------------------------------------------------------- /mobile/src/pages/HelpOrdersPages/HelpOrders/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | import Button from '~/components/Button'; 4 | 5 | export const Container = styled.SafeAreaView` 6 | flex: 1; 7 | padding: 0 30px; 8 | `; 9 | 10 | export const NewOrderButton = styled(Button)` 11 | margin: 20px 0 10px; 12 | `; 13 | 14 | export const HelpOrderList = styled.FlatList.attrs({ 15 | showsVerticalScrollIndicator: false, 16 | contentContainerStyle: { paddingTop: 10, paddingBottom: 10 }, 17 | })``; 18 | -------------------------------------------------------------------------------- /backend/src/database/seeds/20191016115833-admin-user.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@gympoint.com', 11 | password_hash: bcrypt.hashSync('123456', 8), 12 | created_at: new Date(), 13 | updated_at: new Date(), 14 | }, 15 | ], 16 | {} 17 | ); 18 | }, 19 | 20 | down: () => {}, 21 | }; 22 | -------------------------------------------------------------------------------- /mobile/src/config/ReactotronConfig.js: -------------------------------------------------------------------------------- 1 | import Reactotron from 'reactotron-react-native'; 2 | import AsyncStorage from '@react-native-community/async-storage'; 3 | import { reactotronRedux } from 'reactotron-redux'; 4 | import reactotronSaga from 'reactotron-redux-saga'; 5 | 6 | if (__DEV__) { 7 | const tron = Reactotron.setAsyncStorageHandler(AsyncStorage) 8 | .configure() 9 | .useReactNative() 10 | .use(reactotronRedux()) 11 | .use(reactotronSaga()) 12 | .connect(); 13 | 14 | console.tron = tron; 15 | 16 | tron.clear(); 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/app/models/Student.js: -------------------------------------------------------------------------------- 1 | import Sequelize, { Model } from 'sequelize'; 2 | // import { differenceInYears, parseISO } from 'date-fns'; 3 | 4 | class Student extends Model { 5 | static init(sequelize) { 6 | super.init( 7 | { 8 | name: Sequelize.STRING, 9 | email: Sequelize.STRING, 10 | age: Sequelize.INTEGER, 11 | weight: Sequelize.FLOAT, 12 | height: Sequelize.FLOAT, 13 | }, 14 | { 15 | sequelize, 16 | } 17 | ); 18 | return this; 19 | } 20 | } 21 | 22 | export default Student; 23 | -------------------------------------------------------------------------------- /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 | { 12 | sequelize, 13 | } 14 | ); 15 | 16 | return this; 17 | } 18 | 19 | static associate(models) { 20 | this.belongsTo(models.Student, { foreignKey: 'student_id', as: 'student' }); 21 | } 22 | } 23 | 24 | export default Order; 25 | -------------------------------------------------------------------------------- /mobile/src/store/modules/student/actions.js: -------------------------------------------------------------------------------- 1 | export function signInRequest(id) { 2 | return { 3 | type: '@student/SIGN_IN_REQUEST', 4 | payload: { id }, 5 | }; 6 | } 7 | 8 | export function signInSuccess(membership, student) { 9 | return { 10 | type: '@student/SIGN_IN_SUCCESS', 11 | payload: { membership, student }, 12 | }; 13 | } 14 | 15 | export function signFailure() { 16 | return { 17 | type: '@student/SIGN_FAILURE', 18 | }; 19 | } 20 | 21 | export function signOut() { 22 | return { 23 | type: '@student/SIGN_OUT', 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/app/views/emails/confirmation.hbs: -------------------------------------------------------------------------------- 1 | Olá, {{ student }} 2 | 3 |

Sua matricula foi realizada com sucesso, seja bem vindo(a) a GymPoint. 💪

4 |

Confira os detalhes abaixo:

5 |

6 | 📅 Data da Matrícula: {{ start_date }}
7 | 🚀 Plano: {{ plan }}
8 | 💵 Preço Total: {{ price }}
9 | 📆 Vigência: {{ duration }}
10 | 🆔 ID: {{ id }}
11 | Use seu ID para acessar seu perfil no app mobile. 12 |

13 | -------------------------------------------------------------------------------- /mobile/src/pages/HelpOrdersPages/NewOrder/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | import Button from '~/components/Button'; 4 | import Input from '~/components/Input'; 5 | 6 | export const Container = styled.SafeAreaView` 7 | flex: 1; 8 | padding: 0 30px; 9 | margin-top: 20px; 10 | `; 11 | 12 | export const OrderTextArea = styled(Input).attrs({ 13 | textAlignVertical: 'top', 14 | padding: 0, 15 | })` 16 | min-height: 180px; 17 | font-size: 16px; 18 | font-weight: 400; 19 | padding-top: 20px; 20 | line-height: 24px; 21 | `; 22 | 23 | export const SubmitButton = styled(Button)` 24 | margin: 20px 0 10px; 25 | `; 26 | -------------------------------------------------------------------------------- /backend/src/app/jobs/HelpOrderAnswerMail.js: -------------------------------------------------------------------------------- 1 | import Mail from '../../lib/Mail'; 2 | 3 | class HelpOrderAnswerMail { 4 | get key() { 5 | return 'HelpOrderAnswerMail'; 6 | } 7 | 8 | async handle({ data }) { 9 | const { order } = data; 10 | 11 | await Mail.sendMail({ 12 | to: `${order.student.name} <${order.student.email}>`, 13 | subject: 'Resposta para seu pedido na GymPoint', 14 | template: 'help-order-answer', 15 | context: { 16 | student: order.student.name, 17 | question: order.question, 18 | answer: order.answer, 19 | }, 20 | }); 21 | } 22 | } 23 | 24 | export default new HelpOrderAnswerMail(); 25 | -------------------------------------------------------------------------------- /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 | 27 | export function tokenInvalid() { 28 | return { 29 | type: '@auth/TOKEN_INVALID', 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /mobile/src/components/Button/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActivityIndicator } from 'react-native'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import { Container, Text } from './styles'; 6 | 7 | export default function Button({ children, loading, ...rest }) { 8 | return ( 9 | 10 | {loading ? ( 11 | 12 | ) : ( 13 | {children} 14 | )} 15 | 16 | ); 17 | } 18 | 19 | Button.propTypes = { 20 | children: PropTypes.string.isRequired, 21 | loading: PropTypes.bool, 22 | }; 23 | 24 | Button.defaultProps = { 25 | loading: false, 26 | }; 27 | -------------------------------------------------------------------------------- /backend/src/app/middlewares/auth.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import { promisify } from 'util'; 3 | 4 | import authConfig from '../../config/auth'; 5 | 6 | export default async (req, res, next) => { 7 | const authHeader = req.headers.authorization; 8 | 9 | if (!authHeader) { 10 | return res.status(401).json({ error: 'Token not provided' }); 11 | } 12 | 13 | const [, token] = authHeader.split(' '); 14 | 15 | try { 16 | const decodedToken = await promisify(jwt.verify)(token, authConfig.secret); 17 | 18 | req.userId = decodedToken.id; 19 | 20 | return next(); 21 | } catch (err) { 22 | return res.status(401).json({ error: 'Token invalid' }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /mobile/android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /frontend/src/components/Loading/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Spinner, Dots } from 'react-activity'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import colors from '~/styles/colors'; 6 | import { Container } from './styles'; 7 | 8 | const Loading = ({ type }) => { 9 | if (type === 'spinner') { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | } 16 | return ; 17 | }; 18 | 19 | Loading.defaultProps = { 20 | type: '', 21 | }; 22 | 23 | Loading.propTypes = { 24 | type: PropTypes.string, 25 | }; 26 | 27 | export default Loading; 28 | -------------------------------------------------------------------------------- /mobile/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { persistStore } from 'redux-persist'; 2 | import createSagaMiddleware from 'redux-saga'; 3 | import createStore from './createStore'; 4 | import persistReducers from './persistReducers'; 5 | 6 | import rootReducer from './modules/rootReducer'; 7 | import rootSaga from './modules/rootSaga'; 8 | 9 | const sagaMonitor = __DEV__ ? console.tron.createSagaMonitor() : null; 10 | 11 | const sagaMiddleware = createSagaMiddleware({ sagaMonitor }); 12 | 13 | const middleware = [sagaMiddleware]; 14 | 15 | const store = createStore(persistReducers(rootReducer), middleware); 16 | const persistor = persistStore(store); 17 | 18 | sagaMiddleware.run(rootSaga); 19 | 20 | export { store, persistor }; 21 | -------------------------------------------------------------------------------- /mobile/src/pages/SignIn/styles.js: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native'; 2 | import styled from 'styled-components/native'; 3 | 4 | import Input from '~/components/Input'; 5 | import Button from '~/components/Button'; 6 | 7 | export const Container = styled.KeyboardAvoidingView.attrs({ 8 | enabled: Platform.OS === 'ios', 9 | behavior: 'padding', 10 | })` 11 | flex: 1; 12 | justify-content: center; 13 | align-items: center; 14 | padding: 0 30px; 15 | `; 16 | 17 | export const Form = styled.View` 18 | align-self: stretch; 19 | margin-top: 25px; 20 | `; 21 | 22 | export const FormInput = styled(Input)` 23 | margin-bottom: 10px; 24 | `; 25 | 26 | export const SubmitButton = styled(Button)` 27 | margin-top: 5px; 28 | `; 29 | -------------------------------------------------------------------------------- /mobile/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { StatusBar, YellowBox } from 'react-native'; 4 | import FlashMessage from 'react-native-flash-message'; 5 | 6 | import createRouter from './routes'; 7 | 8 | YellowBox.ignoreWarnings(['Unrecognized WebSocket']); 9 | 10 | export default function App() { 11 | const signed = useSelector(state => state.student.signed); 12 | 13 | const Routes = createRouter(signed); 14 | 15 | return ( 16 | <> 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | StatusBar.setTranslucent(true); 24 | StatusBar.setBarStyle('dark-content'); 25 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { persistStore } from 'redux-persist'; 2 | import createSagaMiddleware from 'redux-saga'; 3 | import createStore from './createStore'; 4 | import persistReducers from './persistReducers'; 5 | 6 | import rootReducer from './modules/rootReducer'; 7 | import rootSaga from './modules/rootSaga'; 8 | 9 | const sagaMonitor = 10 | process.env.NODE_ENV === 'development' 11 | ? console.tron.createSagaMonitor() 12 | : null; 13 | 14 | const sagaMiddleware = createSagaMiddleware({ sagaMonitor }); 15 | 16 | const middleware = [sagaMiddleware]; 17 | 18 | const store = createStore(persistReducers(rootReducer), middleware); 19 | const persistor = persistStore(store); 20 | 21 | sagaMiddleware.run(rootSaga); 22 | 23 | export { store, persistor }; 24 | -------------------------------------------------------------------------------- /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: Sequelize.VIRTUAL, 11 | password_hash: Sequelize.STRING, 12 | }, 13 | { 14 | sequelize, 15 | } 16 | ); 17 | 18 | this.addHook('beforeSave', async user => { 19 | if (user.password) { 20 | user.password_hash = await bcrypt.hash(user.password, 8); 21 | } 22 | }); 23 | 24 | return this; 25 | } 26 | 27 | checkPassword(password) { 28 | return bcrypt.compare(password, this.password_hash); 29 | } 30 | } 31 | 32 | export default User; 33 | -------------------------------------------------------------------------------- /mobile/ios/mobile/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /mobile/src/components/Header/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | import logo from '~/assets/logo-header.png'; 4 | 5 | export const Container = styled.View` 6 | align-items: center; 7 | justify-content: center; 8 | padding-top: 15px; 9 | border: 1px solid #ddd; 10 | `; 11 | 12 | export const ContainerWithNav = styled.View` 13 | align-items: center; 14 | justify-content: center; 15 | padding-top: 15px; 16 | flex-direction: row; 17 | border: 1px solid #ddd; 18 | `; 19 | 20 | export const LogoWrapper = styled.View` 21 | flex: 1; 22 | margin-right: 50px; 23 | `; 24 | 25 | export const Logo = styled.Image.attrs({ 26 | source: logo, 27 | })` 28 | margin: 20px 0; 29 | align-self: center; 30 | `; 31 | 32 | export const BackButton = styled.TouchableOpacity` 33 | margin: 0 0 0 20px; 34 | `; 35 | -------------------------------------------------------------------------------- /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 Membership from '../app/models/Membership'; 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, Membership, 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 | -------------------------------------------------------------------------------- /mobile/ios/mobileTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /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 | 7 | import './config/ReactotronConfig'; 8 | 9 | import Routes from './routes'; 10 | import history from './services/history'; 11 | 12 | import { store, persistor } from './store'; 13 | 14 | import GlobalStyle from './styles/global'; 15 | 16 | function App() { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /mobile/ios/mobile-tvOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20191019155243-create-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 | onUpdate: 'CASCADE', 14 | onDelete: 'CASCADE', 15 | allowNull: true, 16 | }, 17 | created_at: { 18 | type: Sequelize.DATE, 19 | allowNull: false, 20 | }, 21 | updated_at: { 22 | type: Sequelize.DATE, 23 | allowNull: false, 24 | }, 25 | }); 26 | }, 27 | 28 | down: queryInterface => { 29 | return queryInterface.dropTable('checkins'); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/java/com/mobile/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.mobile; 2 | 3 | import com.facebook.react.ReactActivity; 4 | import com.facebook.react.ReactActivityDelegate; 5 | import com.facebook.react.ReactRootView; 6 | import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView; 7 | 8 | public class MainActivity extends ReactActivity { 9 | 10 | /** 11 | * Returns the name of the main component registered from JavaScript. This is used to schedule 12 | * rendering of the component. 13 | */ 14 | @Override 15 | protected String getMainComponentName() { 16 | return "mobile"; 17 | } 18 | 19 | @Override 20 | protected ReactActivityDelegate createReactActivityDelegate() { 21 | return new ReactActivityDelegate(this, getMainComponentName()) { 22 | @Override 23 | protected ReactRootView createRootView() { 24 | return new RNGestureHandlerEnabledRootView(MainActivity.this); 25 | } 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20191016121858-create-users.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 | created_at: { 24 | type: Sequelize.DATE, 25 | allowNull: false, 26 | }, 27 | updated_at: { 28 | type: Sequelize.DATE, 29 | allowNull: false, 30 | }, 31 | }); 32 | }, 33 | 34 | down: queryInterface => { 35 | return queryInterface.dropTable('users'); 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20191018151913-plan-management.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 | unique: true, 14 | }, 15 | duration: { 16 | type: Sequelize.INTEGER, 17 | allowNull: false, 18 | }, 19 | price: { 20 | type: Sequelize.DOUBLE, 21 | allowNull: false, 22 | }, 23 | created_at: { 24 | type: Sequelize.DATE, 25 | allowNull: false, 26 | }, 27 | updated_at: { 28 | type: Sequelize.DATE, 29 | allowNull: false, 30 | }, 31 | }); 32 | }, 33 | 34 | down: queryInterface => { 35 | return queryInterface.dropTable('plans'); 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /mobile/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | android.useAndroidX=true 21 | android.enableJetifier=true 22 | -------------------------------------------------------------------------------- /backend/__tests__/factories.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | import { factory } from 'factory-girl'; 3 | 4 | import User from '../src/app/models/User'; 5 | import Student from '../src/app/models/Student'; 6 | import Plan from '../src/app/models/Plan'; 7 | import Checkin from '../src/app/models/Checkin'; 8 | 9 | factory.define('User', User, () => ({ 10 | name: faker.name.findName(), 11 | email: faker.internet.email(), 12 | password: faker.internet.password(), 13 | })); 14 | 15 | factory.define('Student', Student, () => ({ 16 | name: faker.name.findName(), 17 | email: faker.internet.email(), 18 | age: faker.random.number(120), 19 | weight: faker.finance.amount(50, 250, 2), 20 | height: faker.finance.amount(1.2, 2.5, 2), 21 | })); 22 | 23 | factory.define('Plan', Plan, () => ({ 24 | title: faker.random.word(), 25 | duration: faker.random.number({ min: 1, max: 12 }), 26 | price: faker.finance.amount(10, 200, 2), 27 | })); 28 | 29 | factory.define('Checkin', Checkin, {}); 30 | 31 | export default factory; 32 | -------------------------------------------------------------------------------- /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 | case '@auth/TOKEN_INVALID': { 32 | draft.token = null; 33 | draft.signed = false; 34 | break; 35 | } 36 | default: 37 | } 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /mobile/src/store/modules/student/reducer.js: -------------------------------------------------------------------------------- 1 | import produce from 'immer'; 2 | 3 | const INITIAL_STATE = { 4 | profile: null, 5 | membership: null, 6 | signed: false, 7 | loading: false, 8 | }; 9 | 10 | export default function student(state = INITIAL_STATE, action) { 11 | return produce(state, draft => { 12 | switch (action.type) { 13 | case '@student/SIGN_IN_REQUEST': { 14 | draft.loading = true; 15 | break; 16 | } 17 | case '@student/SIGN_IN_SUCCESS': { 18 | draft.profile = action.payload.student; 19 | draft.membership = action.payload.membership; 20 | draft.signed = true; 21 | draft.loading = false; 22 | break; 23 | } 24 | case '@student/SIGN_FAILURE': { 25 | draft.loading = false; 26 | break; 27 | } 28 | case '@student/SIGN_OUT': { 29 | draft.signed = false; 30 | draft.profile = null; 31 | draft.membership = null; 32 | break; 33 | } 34 | default: 35 | } 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/helpers/hooks/useComponentVisible.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react'; 2 | 3 | export default function useComponentVisible(initialIsVisible) { 4 | const [isComponentVisible, setIsComponentVisible] = useState( 5 | initialIsVisible 6 | ); 7 | const ref = useRef(null); 8 | 9 | const handleHideDropdown = event => { 10 | if (event.key === 'Escape') { 11 | setIsComponentVisible(false); 12 | } 13 | }; 14 | 15 | const handleClickOutside = event => { 16 | if (ref.current && !ref.current.contains(event.target)) { 17 | setIsComponentVisible(false); 18 | } 19 | }; 20 | 21 | useEffect(() => { 22 | document.addEventListener('keydown', handleHideDropdown, true); 23 | document.addEventListener('click', handleClickOutside, true); 24 | return () => { 25 | document.removeEventListener('keydown', handleHideDropdown, true); 26 | document.removeEventListener('click', handleClickOutside, true); 27 | }; 28 | }); 29 | 30 | return { ref, isComponentVisible, setIsComponentVisible }; 31 | } 32 | -------------------------------------------------------------------------------- /mobile/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # Android/IntelliJ 25 | # 26 | build/ 27 | .idea 28 | .gradle 29 | local.properties 30 | *.iml 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | yarn-error.log 37 | 38 | # BUCK 39 | buck-out/ 40 | \.buckd/ 41 | *.keystore 42 | !debug.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # CocoaPods 59 | /ios/Pods/ 60 | -------------------------------------------------------------------------------- /mobile/src/pages/CheckInsPage/Checkins/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | import Button from '~/components/Button'; 4 | 5 | export const Container = styled.SafeAreaView` 6 | flex: 1; 7 | padding: 0 30px; 8 | `; 9 | 10 | export const CheckinButton = styled(Button)` 11 | margin: 20px 0 10px; 12 | `; 13 | 14 | export const CheckinList = styled.FlatList.attrs({ 15 | showsVerticalScrollIndicator: false, 16 | contentContainerStyle: { paddingTop: 10, paddingBottom: 10 }, 17 | })``; 18 | 19 | export const Checkin = styled.View` 20 | flex-direction: row; 21 | align-items: center; 22 | justify-content: space-between; 23 | height: 46px; 24 | border-radius: 4px; 25 | border: 1px solid #ddd; 26 | background-color: #fff; 27 | padding: 0 20px; 28 | margin: 5px 0; 29 | `; 30 | 31 | export const Left = styled.Text` 32 | height: 17px; 33 | color: #444; 34 | font-size: 14px; 35 | font-weight: 700; 36 | `; 37 | 38 | export const Right = styled.Text` 39 | height: 17px; 40 | color: #666; 41 | font-size: 14px; 42 | font-weight: 400; 43 | `; 44 | -------------------------------------------------------------------------------- /backend/src/app/models/Membership.js: -------------------------------------------------------------------------------- 1 | import Sequelize, { Model } from 'sequelize'; 2 | import { isBefore, isAfter } from 'date-fns'; 3 | 4 | class Membership extends Model { 5 | static init(sequelize) { 6 | super.init( 7 | { 8 | start_date: Sequelize.DATE, 9 | end_date: Sequelize.DATE, 10 | price: Sequelize.DOUBLE, 11 | active: { 12 | type: Sequelize.VIRTUAL(Sequelize.BOOLEAN, [ 13 | 'start_date', 14 | 'end_date', 15 | ]), 16 | get() { 17 | return ( 18 | isBefore(this.get('start_date'), new Date()) && 19 | isAfter(this.get('end_date'), new Date()) 20 | ); 21 | }, 22 | }, 23 | }, 24 | { 25 | sequelize, 26 | } 27 | ); 28 | 29 | return this; 30 | } 31 | 32 | static associate(models) { 33 | this.belongsTo(models.Student, { foreignKey: 'student_id', as: 'student' }); 34 | this.belongsTo(models.Plan, { foreignKey: 'plan_id', as: 'plan' }); 35 | } 36 | } 37 | 38 | export default Membership; 39 | -------------------------------------------------------------------------------- /backend/src/lib/Queue.js: -------------------------------------------------------------------------------- 1 | import Bee from 'bee-queue'; 2 | import ConfimationMail from '../app/jobs/ConfirmationMail'; 3 | import HelpOrderAnswerMail from '../app/jobs/HelpOrderAnswerMail'; 4 | import redisConfig from '../config/redis'; 5 | 6 | const jobs = [ConfimationMail, HelpOrderAnswerMail]; 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.on('failed', this.handleFailure).process(handle); 35 | }); 36 | } 37 | 38 | handleFailure(job, err) { 39 | console.log(`Queue ${job.queue.name}: FAILED`, err); 40 | } 41 | } 42 | 43 | export default new Queue(); 44 | -------------------------------------------------------------------------------- /frontend/src/styles/global.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | import 'react-toastify/dist/ReactToastify.css'; 4 | import 'react-activity/dist/react-activity.css'; 5 | import 'react-confirm-alert/src/react-confirm-alert.css'; 6 | import 'react-datepicker/dist/react-datepicker.css'; 7 | 8 | export default createGlobalStyle` 9 | @import url('https://fonts.googleapis.com/css?family=Roboto:400,700&display=swap'); 10 | 11 | * { 12 | margin: 0; 13 | padding: 0; 14 | outline: 0; 15 | box-sizing: border-box; 16 | } 17 | *:focus { 18 | outline: 0; 19 | } 20 | html, body, #root { 21 | height: 100%; 22 | } 23 | body { 24 | text-rendering: optimizeLegibility !important; 25 | -webkit-font-smoothing: antialiased !important; 26 | background: #f5f5f5; 27 | } 28 | body, input, button, input::placeholder, textarea::placeholder { 29 | font: 14px 'Roboto', sans-serif;; 30 | } 31 | a { 32 | text-decoration: none; 33 | } 34 | ul { 35 | list-style: none; 36 | } 37 | button { 38 | cursor: pointer; 39 | } 40 | `; 41 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20191019173908-create-help-orders.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.createTable('orders', { 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 | onUpdate: 'CASCADE', 14 | onDelete: 'CASCADE', 15 | allowNull: true, 16 | }, 17 | question: { 18 | type: Sequelize.STRING, 19 | allowNull: false, 20 | }, 21 | answer: { 22 | type: Sequelize.STRING, 23 | }, 24 | answer_at: { 25 | type: Sequelize.DATE, 26 | }, 27 | created_at: { 28 | type: Sequelize.DATE, 29 | allowNull: false, 30 | }, 31 | updated_at: { 32 | type: Sequelize.DATE, 33 | allowNull: false, 34 | }, 35 | }); 36 | }, 37 | 38 | down: queryInterface => { 39 | return queryInterface.dropTable('orders'); 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Stefano Akira Saffran 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontend/src/routes/Route.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Route, Redirect } from 'react-router-dom'; 4 | 5 | import AuthLayout from '~/pages/_layouts/auth'; 6 | import DefaultLayout from '~/pages/_layouts/default'; 7 | 8 | import { store } from '~/store'; 9 | 10 | const RouteWrapper = ({ component: Component, isPrivate, ...rest }) => { 11 | const { signed } = store.getState().auth; 12 | 13 | if (!signed && isPrivate) { 14 | return ; 15 | } 16 | 17 | if (signed && !isPrivate) { 18 | return ; 19 | } 20 | 21 | const Layout = signed ? DefaultLayout : AuthLayout; 22 | 23 | return ( 24 | ( 27 | 28 | 29 | 30 | )} 31 | /> 32 | ); 33 | }; 34 | 35 | RouteWrapper.propTypes = { 36 | isPrivate: PropTypes.bool, 37 | component: PropTypes.oneOfType([PropTypes.element, PropTypes.func]) 38 | .isRequired, 39 | }; 40 | 41 | RouteWrapper.defaultProps = { 42 | isPrivate: false, 43 | }; 44 | 45 | export default RouteWrapper; 46 | -------------------------------------------------------------------------------- /mobile/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "28.0.3" 6 | minSdkVersion = 16 7 | compileSdkVersion = 28 8 | targetSdkVersion = 28 9 | } 10 | repositories { 11 | google() 12 | jcenter() 13 | } 14 | dependencies { 15 | classpath("com.android.tools.build:gradle:3.4.2") 16 | 17 | // NOTE: Do not place your application dependencies here; they belong 18 | // in the individual module build.gradle files 19 | } 20 | } 21 | 22 | allprojects { 23 | repositories { 24 | mavenLocal() 25 | maven { 26 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 27 | url("$rootDir/../node_modules/react-native/android") 28 | } 29 | maven { 30 | // Android JSC is installed from npm 31 | url("$rootDir/../node_modules/jsc-android/dist") 32 | } 33 | 34 | google() 35 | jcenter() 36 | maven { url 'https://jitpack.io' } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/components/Header/Menu/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MdMenu, MdClose } from 'react-icons/md'; 3 | 4 | import useComponentVisible from '~/helpers/hooks/useComponentVisible'; 5 | import LinkWrapper from '~/helpers/LinkWrapper'; 6 | 7 | import { Container } from './styles'; 8 | 9 | export default function Menu() { 10 | const { 11 | ref, 12 | isComponentVisible, 13 | setIsComponentVisible, 14 | } = useComponentVisible(false); 15 | 16 | return ( 17 | 18 | 21 | 22 | {isComponentVisible ? ( 23 | <> 24 |
25 | ALUNOS 26 | PLANOS 27 | MATRÍCULAS 28 | PEDIDOS DE AUXÍLIO 29 |
30 |
31 | 32 | ) : null} 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20191016133943-create-students.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.createTable('students', { 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 | age: { 20 | type: Sequelize.INTEGER, 21 | allowNull: false, 22 | }, 23 | weight: { 24 | type: Sequelize.FLOAT, 25 | allowNull: false, 26 | }, 27 | height: { 28 | type: Sequelize.FLOAT, 29 | allowNull: false, 30 | }, 31 | created_at: { 32 | type: Sequelize.DATE, 33 | allowNull: false, 34 | }, 35 | updated_at: { 36 | type: Sequelize.DATE, 37 | allowNull: false, 38 | }, 39 | }); 40 | }, 41 | 42 | down: queryInterface => { 43 | return queryInterface.dropTable('students'); 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /frontend/src/assets/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | zoom-2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend/src/components/Pagination/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { darken } from 'polished'; 3 | 4 | import colors from '~/styles/colors'; 5 | 6 | export const Container = styled.div` 7 | margin-top: 15px; 8 | display: flex; 9 | flex-direction: row !important; 10 | justify-content: center !important; 11 | align-items: center; 12 | 13 | button:first-child { 14 | margin-right: 10px; 15 | } 16 | button:last-child { 17 | margin-left: 10px; 18 | } 19 | 20 | button { 21 | padding: 0 10px; 22 | height: 36px; 23 | line-height: 16px; 24 | border: 0; 25 | border-radius: 4px; 26 | color: ${colors.white}; 27 | background: ${colors.primary}; 28 | 29 | &:hover:not(:disabled) { 30 | background: ${darken(0.06, `${colors.primary}`)}; 31 | } 32 | 33 | :disabled { 34 | opacity: 0.6; 35 | cursor: not-allowed; 36 | } 37 | } 38 | 39 | p { 40 | padding: 5px 10px; 41 | height: 36px; 42 | text-align: center; 43 | line-height: 26px; 44 | background: ${colors.white}; 45 | border: 1px solid ${colors.lightGray}; 46 | border-radius: 4px; 47 | color: ${colors.darkGray}; 48 | margin: 0 10px; 49 | } 50 | `; 51 | -------------------------------------------------------------------------------- /backend/src/app/jobs/ConfirmationMail.js: -------------------------------------------------------------------------------- 1 | import { format, parseISO } from 'date-fns'; 2 | import pt from 'date-fns/locale/pt'; 3 | import Mail from '../../lib/Mail'; 4 | 5 | class ConfirmationMail { 6 | get key() { 7 | return 'ConfirmationMail'; 8 | } 9 | 10 | async handle({ data }) { 11 | const { membershipInfo } = data; 12 | 13 | await Mail.sendMail({ 14 | to: `${membershipInfo.student.name} <${membershipInfo.student.email}>`, 15 | subject: 'Matricula realizada com sucesso', 16 | template: 'confirmation', 17 | context: { 18 | student: membershipInfo.student.name, 19 | start_date: format( 20 | parseISO(membershipInfo.start_date), 21 | "dd 'de' MMMM 'de' yyyy", 22 | { 23 | locale: pt, 24 | } 25 | ), 26 | id: membershipInfo.student.id, 27 | plan: membershipInfo.plan.title, 28 | price: membershipInfo.price, 29 | duration: format( 30 | parseISO(membershipInfo.end_date), 31 | "'Até dia' dd 'de' MMMM 'de' yyyy", 32 | { 33 | locale: pt, 34 | } 35 | ), 36 | }, 37 | }); 38 | } 39 | } 40 | 41 | export default new ConfirmationMail(); 42 | -------------------------------------------------------------------------------- /backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true, 5 | jest: true, 6 | }, 7 | extends: [ 8 | 'airbnb-base', 'prettier' 9 | ], 10 | plugins: [ 'prettier', 'jest' ], 11 | globals: { 12 | Atomics: 'readonly', 13 | SharedArrayBuffer: 'readonly', 14 | }, 15 | parserOptions: { 16 | ecmaVersion: 2018, 17 | sourceType: 'module', 18 | }, 19 | rules: { 20 | "prettier/prettier": "error", 21 | "newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 }], 22 | "semi": 0, 23 | "eqeqeq": "error", 24 | "no-trailing-spaces": "error", 25 | "object-curly-spacing": [ 26 | "error", "always" 27 | ], 28 | "arrow-spacing": [ 29 | "error", { "before": true, "after": true } 30 | ], 31 | "no-console": 0, 32 | "react/prop-types": 0, 33 | "class-methods-use-this": "off", 34 | "no-param-reassign": "off", 35 | "camelcase": "off", 36 | "no-unused-vars": ["error", { "argsIgnorePattern": "next" }], 37 | "jest/no-disabled-tests": "warn", 38 | "jest/no-focused-tests": "error", 39 | "jest/no-identical-title": "error", 40 | "jest/prefer-to-have-length": "warn", 41 | "jest/valid-expect": "error" 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /backend/src/lib/Mail.js: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | import { resolve } from 'path'; 3 | import exphbs from 'express-handlebars'; 4 | import nodemailerhbs from 'nodemailer-express-handlebars'; 5 | import mailConfig from '../config/mail'; 6 | 7 | class Mail { 8 | constructor() { 9 | const { host, port, secure, auth } = mailConfig; 10 | 11 | this.transporter = nodemailer.createTransport({ 12 | host, 13 | port, 14 | secure, 15 | auth: auth.user ? auth : null, 16 | }); 17 | 18 | this.configureTemplates(); 19 | } 20 | 21 | configureTemplates() { 22 | const viewPath = resolve(__dirname, '..', 'app', 'views', 'emails'); 23 | 24 | this.transporter.use( 25 | 'compile', 26 | nodemailerhbs({ 27 | viewEngine: exphbs.create({ 28 | layoutsDir: resolve(viewPath, 'layouts'), 29 | partialsDir: resolve(viewPath, 'partials'), 30 | defaultLayout: 'default', 31 | extname: '.hbs', 32 | }), 33 | viewPath, 34 | extName: '.hbs', 35 | }) 36 | ); 37 | } 38 | 39 | sendMail(message) { 40 | return this.transporter.sendMail({ 41 | ...mailConfig.default, 42 | ...message, 43 | }); 44 | } 45 | } 46 | 47 | export default new Mail(); 48 | -------------------------------------------------------------------------------- /mobile/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | }, 5 | extends: [ 6 | 'airbnb', 7 | 'prettier', 8 | 'prettier/react' 9 | ], 10 | globals: { 11 | Atomics: 'readonly', 12 | SharedArrayBuffer: 'readonly', 13 | __DEV__: 'readonly', 14 | }, 15 | parser: 'babel-eslint', 16 | parserOptions: { 17 | ecmaFeatures: { 18 | jsx: true, 19 | }, 20 | ecmaVersion: 2018, 21 | sourceType: 'module', 22 | }, 23 | plugins: [ 24 | 'react', 25 | 'prettier', 26 | 'react-hooks' 27 | ], 28 | rules: { 29 | 'prettier/prettier': 'error', 30 | 'react/jsx-filename-extension': [ 31 | 'warn', 32 | { extensions: ['.jsx', '.js' ] } 33 | ], 34 | 'import/prefer-default-export': 'off', 35 | "react/state-in-constructor": "off", 36 | 'react/static-property-placement':'off', 37 | 'react/jsx-props-no-spreading': 'off', 38 | 'no-param-reassign': 'off', 39 | 'no-console': ['error', { allow: ['tron'] }], 40 | 41 | 'react-hooks/rules-of-hooks': 'error', 42 | 'react-hooks/exhaustive-deps': 'warn', 43 | }, 44 | settings: { 45 | "import/resolver": { 46 | "babel-plugin-root-import": { 47 | rootPathSuffix: "src" 48 | }, 49 | }, 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /mobile/src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from 'react-native-vector-icons/MaterialIcons'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import { 6 | Container, 7 | ContainerWithNav, 8 | Logo, 9 | LogoWrapper, 10 | BackButton, 11 | } from './styles'; 12 | 13 | const HeaderLogo = ({ navigation }) => { 14 | return ( 15 | <> 16 | {!navigation || navigation.state.routeName === 'HelpOrders' ? ( 17 | 18 | 19 | 20 | ) : ( 21 | 22 | { 24 | navigation.navigate('HelpOrders'); 25 | }} 26 | > 27 | 28 | 29 | 30 | 31 | 32 | 33 | )} 34 | 35 | ); 36 | }; 37 | 38 | HeaderLogo.propTypes = { 39 | navigation: PropTypes.shape({ 40 | navigate: PropTypes.func, 41 | state: PropTypes.shape({ 42 | routeName: PropTypes.string, 43 | }), 44 | }), 45 | }; 46 | 47 | HeaderLogo.defaultProps = { 48 | navigation: null, 49 | }; 50 | 51 | export default HeaderLogo; 52 | -------------------------------------------------------------------------------- /mobile/src/components/Order/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | import { RectButton } from 'react-native-gesture-handler'; 3 | 4 | export const Container = styled.View` 5 | border: 1px solid #ddd; 6 | background: #fff; 7 | border-radius: 4px; 8 | margin: 0 0 10px; 9 | `; 10 | 11 | export const NewOrder = styled(RectButton)` 12 | flex-direction: column; 13 | padding: 20px 20px 25px; 14 | `; 15 | 16 | export const Top = styled.View` 17 | flex-direction: row; 18 | justify-content: space-between; 19 | align-items: center; 20 | `; 21 | 22 | export const Status = styled.View` 23 | flex-direction: row; 24 | justify-content: space-between; 25 | align-items: center; 26 | height: 17px; 27 | color: #999; 28 | font-size: 14px; 29 | font-weight: 700; 30 | `; 31 | 32 | export const StatusText = styled.Text` 33 | margin-left: 8px; 34 | font-size: 14px; 35 | font-weight: 700 bold; 36 | color: ${props => (props.answered ? '#42cb59' : '#999')}; 37 | `; 38 | 39 | export const Time = styled.Text` 40 | color: #666; 41 | font-size: 14px; 42 | font-weight: 400; 43 | `; 44 | 45 | export const Question = styled.Text` 46 | margin-top: 15px; 47 | line-height: 26px; 48 | color: #666; 49 | font-size: 14px; 50 | font-weight: 400; 51 | `; 52 | -------------------------------------------------------------------------------- /mobile/src/components/Order/index.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import Icon from 'react-native-vector-icons/MaterialIcons'; 3 | import { parseISO, formatRelative } from 'date-fns'; 4 | import pt from 'date-fns/locale/pt-BR'; 5 | 6 | import { 7 | Container, 8 | NewOrder, 9 | Top, 10 | Status, 11 | StatusText, 12 | Time, 13 | Question, 14 | } from './styles'; 15 | 16 | export default function Order({ data, onClick }) { 17 | const dateParsed = useMemo(() => { 18 | return formatRelative(parseISO(data.createdAt), new Date(), { 19 | locale: pt, 20 | addSuffix: true, 21 | }); 22 | }, [data.createdAt]); 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 34 | 35 | {data.answer ? 'Respondido' : 'Sem resposta'} 36 | 37 | 38 | 39 | 40 | {data.question} 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /backend/src/app/controllers/SessionController.js: -------------------------------------------------------------------------------- 1 | import { object, string } from 'yup'; 2 | import jwt from 'jsonwebtoken'; 3 | 4 | import User from '../models/User'; 5 | import authConfig from '../../config/auth'; 6 | 7 | class SessionController { 8 | async store(req, res) { 9 | const schema = object().shape({ 10 | email: string() 11 | .email() 12 | .required(), 13 | password: string().required(), 14 | }); 15 | 16 | if (!(await schema.isValid(req.body))) { 17 | return res.status(400).json({ error: 'validation fails' }); 18 | } 19 | 20 | const { email, password } = req.body; 21 | 22 | const user = await User.findOne({ where: { email } }); 23 | 24 | if (!user) { 25 | return res.status(401).json({ error: 'User not found' }); 26 | } 27 | 28 | if (!(await user.checkPassword(password))) { 29 | return res.status(401).json({ error: 'Password does not match' }); 30 | } 31 | 32 | const { id, name } = user; 33 | 34 | return res.json({ 35 | user: { 36 | id, 37 | name, 38 | email, 39 | }, 40 | token: jwt.sign({ id }, authConfig.secret, { 41 | expiresIn: authConfig.expiresIn, 42 | }), 43 | }); 44 | } 45 | } 46 | 47 | export default new SessionController(); 48 | -------------------------------------------------------------------------------- /mobile/src/store/modules/student/sagas.js: -------------------------------------------------------------------------------- 1 | import { takeLatest, call, all, put } from 'redux-saga/effects'; 2 | import { showMessage } from 'react-native-flash-message'; 3 | 4 | import api from '~/services/api'; 5 | import { signInSuccess, signFailure, signOut } from './actions'; 6 | 7 | export function* signIn({ payload }) { 8 | try { 9 | const { id } = payload; 10 | if (!id) { 11 | yield put(signFailure()); 12 | return; 13 | } 14 | const { data } = yield call(api.get, `students/${id}/checkins`); 15 | yield put(signInSuccess(data.membership, data.student)); 16 | } catch (err) { 17 | yield put(signFailure()); 18 | showMessage({ 19 | message: 'Falha ao realizar login', 20 | description: err.response 21 | ? err.response.data.error 22 | : 'Erro de conexão com o servidor', 23 | type: 'danger', 24 | }); 25 | } 26 | } 27 | 28 | export function* verifyUser({ payload }) { 29 | const { student } = payload; 30 | try { 31 | yield call(api.get, `students/${student.profile.id}/checkins`); 32 | } catch (error) { 33 | if (error.response && error.response.status === 401) { 34 | yield put(signOut()); 35 | } 36 | } 37 | } 38 | 39 | export default all([ 40 | takeLatest('persist/REHYDRATE', verifyUser), 41 | takeLatest('@student/SIGN_IN_REQUEST', signIn), 42 | ]); 43 | -------------------------------------------------------------------------------- /mobile/src/pages/HelpOrdersPages/Answer/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Container = styled.SafeAreaView` 4 | margin: 22px 20px; 5 | border: 1px solid #ddd; 6 | background: #fff; 7 | border-radius: 4px; 8 | `; 9 | 10 | export const Question = styled.View` 11 | padding: 20px 20px 25px; 12 | `; 13 | 14 | export const QuestionHeader = styled.View` 15 | flex-direction: row; 16 | justify-content: space-between; 17 | margin-bottom: 15px; 18 | `; 19 | 20 | export const QuestionText = styled.Text` 21 | color: #444; 22 | font-size: 14px; 23 | font-weight: 700 bold; 24 | text-transform: uppercase; 25 | `; 26 | 27 | export const Time = styled.Text` 28 | color: #666; 29 | font-size: 14px; 30 | font-weight: 400; 31 | `; 32 | 33 | export const QuestionBody = styled.Text` 34 | line-height: 26px; 35 | color: #666; 36 | font-size: 14px; 37 | font-weight: 400; 38 | `; 39 | 40 | export const AnswerWrapper = styled.View` 41 | padding: 0 20px 27px; 42 | `; 43 | 44 | export const AnswerHeader = styled.Text` 45 | margin-bottom: 15px; 46 | color: #444; 47 | font-size: 14px; 48 | font-weight: 700 bold; 49 | text-transform: uppercase; 50 | `; 51 | 52 | export const AnswerBody = styled.Text` 53 | line-height: 26px; 54 | color: #666; 55 | font-size: 14px; 56 | font-weight: 400; 57 | `; 58 | -------------------------------------------------------------------------------- /mobile/src/pages/SignIn/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Image } from 'react-native'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | 5 | import logo from '~/assets/8-layers.png'; 6 | 7 | import Background from '~/components/Background/BackgroundSignIn'; 8 | import { signInRequest } from '~/store/modules/student/actions'; 9 | 10 | import { Container, Form, FormInput, SubmitButton } from './styles'; 11 | 12 | export default function SignIn() { 13 | const [id, setId] = useState(''); 14 | 15 | const dispatch = useDispatch(); 16 | 17 | const loading = useSelector(state => state.student.loading); 18 | 19 | const handleSubmit = () => { 20 | dispatch(signInRequest(id)); 21 | }; 22 | return ( 23 | 24 | 25 | 26 |
27 | 36 | 37 | Entrar no sistema 38 | 39 | 40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/components/Pagination/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | FaAngleDoubleLeft, 5 | FaAngleDoubleRight, 6 | FaAngleRight, 7 | FaAngleLeft, 8 | } from 'react-icons/fa'; 9 | 10 | import { Container } from './styles'; 11 | 12 | export default function Pagination({ page, totalPages, setPage }) { 13 | return ( 14 | 15 | 18 | 25 |

{page}

26 | 33 | 40 |
41 | ); 42 | } 43 | 44 | Pagination.propTypes = { 45 | page: PropTypes.number.isRequired, 46 | totalPages: PropTypes.number.isRequired, 47 | setPage: PropTypes.func.isRequired, 48 | }; 49 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20191018173726-membership-management.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.createTable('memberships', { 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 | onUpdate: 'CASCADE', 14 | onDelete: 'CASCADE', 15 | allowNull: true, 16 | }, 17 | plan_id: { 18 | type: Sequelize.INTEGER, 19 | references: { model: 'plans', key: 'id' }, 20 | onUpdate: 'CASCADE', 21 | onDelete: 'SET NULL', 22 | allowNull: true, 23 | }, 24 | start_date: { 25 | type: Sequelize.DATE, 26 | allowNull: false, 27 | }, 28 | end_date: { 29 | type: Sequelize.DATE, 30 | allowNull: false, 31 | }, 32 | price: { 33 | type: Sequelize.DOUBLE, 34 | allowNull: false, 35 | }, 36 | created_at: { 37 | type: Sequelize.DATE, 38 | allowNull: false, 39 | }, 40 | updated_at: { 41 | type: Sequelize.DATE, 42 | allowNull: false, 43 | }, 44 | }); 45 | }, 46 | 47 | down: queryInterface => { 48 | return queryInterface.dropTable('memberships'); 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /frontend/src/components/HeightInput/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import NumberFormat from 'react-number-format'; 3 | import { useField } from '@rocketseat/unform'; 4 | import PropTypes from 'prop-types'; 5 | 6 | export default function Height({ name, setChange }) { 7 | const ref = useRef(); 8 | const { fieldName, defaultValue, registerField, error } = useField(name); 9 | const [value, setValue] = useState(defaultValue); 10 | 11 | useEffect(() => { 12 | if (ref.current) { 13 | registerField({ 14 | name: fieldName, 15 | ref: ref.current, 16 | path: 'props.value', 17 | }); 18 | } 19 | }, [ref, fieldName]); // eslint-disable-line 20 | 21 | return ( 22 | <> 23 | { 33 | setValue(values.floatValue); 34 | if (setChange) { 35 | setChange(values.floatValue); 36 | } 37 | }} 38 | /> 39 | {error && {error}} 40 | 41 | ); 42 | } 43 | 44 | Height.defaultProps = { 45 | setChange: null, 46 | }; 47 | 48 | Height.propTypes = { 49 | name: PropTypes.string.isRequired, 50 | setChange: PropTypes.func, 51 | }; 52 | -------------------------------------------------------------------------------- /mobile/src/pages/HelpOrdersPages/Answer/index.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { parseISO, formatRelative } from 'date-fns'; 3 | import pt from 'date-fns/locale/pt-BR'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import Background from '~/components/Background'; 7 | import { 8 | Container, 9 | Question, 10 | QuestionHeader, 11 | QuestionText, 12 | Time, 13 | QuestionBody, 14 | AnswerWrapper, 15 | AnswerHeader, 16 | AnswerBody, 17 | } from './styles'; 18 | 19 | export default function Answer({ navigation }) { 20 | const order = navigation.getParam('order'); 21 | 22 | const dateParsed = useMemo(() => { 23 | return formatRelative(parseISO(order.createdAt), new Date(), { 24 | locale: pt, 25 | addSuffix: true, 26 | }); 27 | }, [order.createdAt]); 28 | 29 | return ( 30 | 31 | 32 | 33 | 34 | pergunta 35 | 36 | 37 | {order.question} 38 | 39 | 40 | resposta 41 | {order.answer} 42 | 43 | 44 | 45 | ); 46 | } 47 | 48 | Answer.propTypes = { 49 | navigation: PropTypes.shape({ 50 | getParam: PropTypes.func, 51 | }).isRequired, 52 | }; 53 | -------------------------------------------------------------------------------- /frontend/src/components/Modal/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useImperativeHandle } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import useComponentVisible from '~/helpers/hooks/useComponentVisible'; 4 | 5 | import { Container } from './styles'; 6 | 7 | const Modal = React.forwardRef((props, forwardRef) => { 8 | const { 9 | ref, 10 | isComponentVisible, 11 | setIsComponentVisible, 12 | } = useComponentVisible(false); 13 | const [answer, setAnswer] = useState(''); 14 | 15 | const showHidestyle = { display: isComponentVisible ? '' : 'none' }; 16 | 17 | useImperativeHandle(forwardRef, () => { 18 | return { 19 | setIsComponentVisible, 20 | }; 21 | }); 22 | 23 | return ( 24 | 25 |
26 | {props.children} 27 |
28 | SUA RESPOSTA 29 |