├── mobile ├── .watchmanconfig ├── .gitattributes ├── app.json ├── .prettierrc.js ├── src │ ├── assets │ │ ├── logo.png │ │ ├── logo2.png │ │ ├── logobar.png │ │ ├── logobar@2x.png │ │ └── logobar@3x.png │ ├── services │ │ └── api.js │ ├── store │ │ ├── modules │ │ │ ├── rootReducer.js │ │ │ ├── rootSaga.js │ │ │ └── auth │ │ │ │ ├── action.js │ │ │ │ ├── reducer.js │ │ │ │ └── sagas.js │ │ ├── persistReducers.js │ │ ├── createStore.js │ │ └── index.js │ ├── pages │ │ ├── HelpOrders │ │ │ ├── NewOrder │ │ │ │ ├── styles.js │ │ │ │ └── index.js │ │ │ ├── Order │ │ │ │ ├── index.js │ │ │ │ └── styles.js │ │ │ ├── styles.js │ │ │ └── index.js │ │ ├── SignIn │ │ │ ├── styles.js │ │ │ └── index.js │ │ └── Checkin │ │ │ ├── styles.js │ │ │ └── index.js │ ├── components │ │ ├── Header │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── Button │ │ │ ├── styles.js │ │ │ └── index.js │ │ ├── Input │ │ │ ├── styles.js │ │ │ └── index.js │ │ └── MultilineInput │ │ │ ├── styles.js │ │ │ └── index.js │ ├── App.js │ ├── index.js │ ├── config │ │ └── ReactotronConfig.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 │ │ │ │ ├── assets │ │ │ │ │ └── fonts │ │ │ │ │ │ ├── Entypo.ttf │ │ │ │ │ │ ├── Feather.ttf │ │ │ │ │ │ ├── Zocial.ttf │ │ │ │ │ │ ├── AntDesign.ttf │ │ │ │ │ │ ├── EvilIcons.ttf │ │ │ │ │ │ ├── Fontisto.ttf │ │ │ │ │ │ ├── Ionicons.ttf │ │ │ │ │ │ ├── Octicons.ttf │ │ │ │ │ │ ├── FontAwesome.ttf │ │ │ │ │ │ ├── Foundation.ttf │ │ │ │ │ │ ├── MaterialIcons.ttf │ │ │ │ │ │ ├── SimpleLineIcons.ttf │ │ │ │ │ │ ├── FontAwesome5_Brands.ttf │ │ │ │ │ │ ├── FontAwesome5_Solid.ttf │ │ │ │ │ │ ├── FontAwesome5_Regular.ttf │ │ │ │ │ │ └── MaterialCommunityIcons.ttf │ │ │ │ ├── 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 ├── index.js ├── babel.config.js ├── metro.config.js ├── .gitignore ├── .eslintrc.js ├── README.md ├── package.json └── .flowconfig ├── backend ├── src │ ├── server.js │ ├── config │ │ ├── sentry.js │ │ ├── auth.js │ │ ├── redis.js │ │ ├── mail.js │ │ └── database.js │ ├── queue.js │ ├── app │ │ ├── views │ │ │ └── emails │ │ │ │ ├── assets │ │ │ │ └── logo.png │ │ │ │ ├── partials │ │ │ │ ├── footer.hbs │ │ │ │ └── header.hbs │ │ │ │ ├── helporder.hbs │ │ │ │ ├── enrollment.hbs │ │ │ │ └── layouts │ │ │ │ ├── styles.css │ │ │ │ └── default.hbs │ │ ├── models │ │ │ ├── Checkin.js │ │ │ ├── Plan.js │ │ │ ├── Student.js │ │ │ ├── HelpOrder.js │ │ │ ├── User.js │ │ │ └── Enrollment.js │ │ ├── jobs │ │ │ ├── HelpOrderAnsweredMail.js │ │ │ └── NewEnrollmentMail.js │ │ ├── middleware │ │ │ └── auth.js │ │ └── controllers │ │ │ ├── SessionController.js │ │ │ ├── CheckinController.js │ │ │ ├── PlanController.js │ │ │ ├── UserController.js │ │ │ ├── StudentController.js │ │ │ ├── HelpOrderController.js │ │ │ └── EnrollmentController.js │ ├── database │ │ ├── seeds │ │ │ ├── 20191027212532-admin-user.js │ │ │ └── 20191030001035-add-plans.js │ │ ├── index.js │ │ └── migrations │ │ │ ├── 20191105002554-create-checkins.js │ │ │ ├── 20191027213417-create-users.js │ │ │ ├── 20191030000254-create-plans.js │ │ │ ├── 20191029013007-create-students.js │ │ │ ├── 20191106000601-create-help-orders.js │ │ │ └── 20191031012441-create-enrollment.js │ ├── lib │ │ ├── Queue.js │ │ └── Mail.js │ ├── app.js │ └── routes.js ├── .prettierrc ├── nodemon.json ├── .env.example ├── .sequelizerc ├── .eslintrc.js ├── package.json └── README.md ├── frontend ├── .prettierrc ├── src │ ├── assets │ │ └── logo.png │ ├── services │ │ ├── history.js │ │ ├── api.js │ │ └── error.js │ ├── store │ │ ├── modules │ │ │ ├── rootReducer.js │ │ │ ├── rootSaga.js │ │ │ └── auth │ │ │ │ ├── action.js │ │ │ │ ├── reducer.js │ │ │ │ └── sagas.js │ │ ├── persistReducers.js │ │ ├── createStore.js │ │ └── index.js │ ├── pages │ │ ├── _layouts │ │ │ ├── default │ │ │ │ ├── styles.js │ │ │ │ └── index.js │ │ │ └── auth │ │ │ │ ├── index.js │ │ │ │ └── styles.js │ │ ├── Dashboard │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── SignIn │ │ │ └── index.js │ │ ├── Plans │ │ │ ├── styles.js │ │ │ ├── FormPlan │ │ │ │ ├── styles.js │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── Enrollments │ │ │ ├── styles.js │ │ │ ├── FormEnrollment │ │ │ │ └── styles.js │ │ │ └── index.js │ │ ├── Students │ │ │ ├── FormStudent │ │ │ │ ├── styles.js │ │ │ │ └── index.js │ │ │ ├── styles.js │ │ │ └── index.js │ │ └── HelpOrders │ │ │ ├── styles.js │ │ │ └── index.js │ ├── index.js │ ├── config │ │ └── ReactotronConfig.js │ ├── App.js │ ├── styles │ │ └── global.js │ ├── routes │ │ ├── Route.js │ │ └── index.js │ └── components │ │ └── Header │ │ ├── index.js │ │ └── styles.js ├── jsconfig.json ├── .editorconfig ├── config-overrides.js ├── .gitignore ├── public │ └── index.html ├── .eslintrc.js ├── README.md └── package.json ├── .gitignore └── README.md /mobile/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /mobile/.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /backend/src/server.js: -------------------------------------------------------------------------------- 1 | import app from './app'; 2 | 3 | app.listen(3334); 4 | -------------------------------------------------------------------------------- /mobile/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobile", 3 | "displayName": "mobile" 4 | } 5 | -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /backend/src/config/sentry.js: -------------------------------------------------------------------------------- 1 | export default { 2 | dsn: process.env.SENTRY_DSN, 3 | }; 4 | -------------------------------------------------------------------------------- /backend/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "execMap": { 3 | "js": "node -r sucrase/register" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /mobile/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'es5', 4 | }; 5 | -------------------------------------------------------------------------------- /mobile/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/src/assets/logo.png -------------------------------------------------------------------------------- /mobile/src/assets/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/src/assets/logo2.png -------------------------------------------------------------------------------- /backend/src/config/auth.js: -------------------------------------------------------------------------------- 1 | export default { 2 | secret: process.env.APP_SECRET, 3 | expiresIn: '30d', 4 | }; 5 | -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /mobile/src/assets/logobar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/src/assets/logobar.png -------------------------------------------------------------------------------- /backend/src/queue.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | import Queue from './lib/Queue'; 4 | 5 | Queue.processQueue(); 6 | -------------------------------------------------------------------------------- /mobile/src/assets/logobar@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/src/assets/logobar@2x.png -------------------------------------------------------------------------------- /mobile/src/assets/logobar@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/src/assets/logobar@3x.png -------------------------------------------------------------------------------- /backend/src/config/redis.js: -------------------------------------------------------------------------------- 1 | export default { 2 | host: process.env.REDIS_HOST, 3 | port: process.env.REDIS_PORT, 4 | }; 5 | -------------------------------------------------------------------------------- /mobile/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/android/app/debug.keystore -------------------------------------------------------------------------------- /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/src/app/views/emails/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/backend/src/app/views/emails/assets/logo.png -------------------------------------------------------------------------------- /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/NathanFRibeiro/gympoint/HEAD/mobile/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /mobile/android/app/src/main/assets/fonts/Entypo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/assets/fonts/Entypo.ttf -------------------------------------------------------------------------------- /mobile/android/app/src/main/assets/fonts/Feather.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/assets/fonts/Feather.ttf -------------------------------------------------------------------------------- /mobile/android/app/src/main/assets/fonts/Zocial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/assets/fonts/Zocial.ttf -------------------------------------------------------------------------------- /frontend/src/services/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history'; 2 | 3 | const history = createBrowserHistory(); 4 | 5 | export default history; 6 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/assets/fonts/AntDesign.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/assets/fonts/AntDesign.ttf -------------------------------------------------------------------------------- /mobile/android/app/src/main/assets/fonts/EvilIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/assets/fonts/EvilIcons.ttf -------------------------------------------------------------------------------- /mobile/android/app/src/main/assets/fonts/Fontisto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/assets/fonts/Fontisto.ttf -------------------------------------------------------------------------------- /mobile/android/app/src/main/assets/fonts/Ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/assets/fonts/Ionicons.ttf -------------------------------------------------------------------------------- /mobile/android/app/src/main/assets/fonts/Octicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/assets/fonts/Octicons.ttf -------------------------------------------------------------------------------- /frontend/src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const api = axios.create({ 4 | baseURL: 'http://localhost:3334/', 5 | }); 6 | 7 | export default api; 8 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/assets/fonts/FontAwesome.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/assets/fonts/FontAwesome.ttf -------------------------------------------------------------------------------- /mobile/android/app/src/main/assets/fonts/Foundation.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/assets/fonts/Foundation.ttf -------------------------------------------------------------------------------- /mobile/android/app/src/main/assets/fonts/MaterialIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/assets/fonts/MaterialIcons.ttf -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/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/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const api = axios.create({ 4 | baseURL: 'http://192.168.1.36:3334/', 5 | }); 6 | 7 | export default api; 8 | -------------------------------------------------------------------------------- /frontend/src/store/modules/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import auth from './auth/reducer'; 4 | 5 | export default combineReducers({ auth }); 6 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/assets/fonts/SimpleLineIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/assets/fonts/SimpleLineIcons.ttf -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/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/NathanFRibeiro/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/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/src/store/modules/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import auth from './auth/reducer'; 4 | 5 | export default combineReducers({ auth }); 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/assets/fonts/FontAwesome5_Brands.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf -------------------------------------------------------------------------------- /mobile/android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf -------------------------------------------------------------------------------- /backend/src/app/views/emails/partials/footer.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf -------------------------------------------------------------------------------- /mobile/android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/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/NathanFRibeiro/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/NathanFRibeiro/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/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanFRibeiro/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /frontend/src/store/modules/rootSaga.js: -------------------------------------------------------------------------------- 1 | import { all } from 'redux-saga/effects'; 2 | 3 | import auth from './auth/sagas'; 4 | 5 | export default function* rootSaga() { 6 | return yield all([auth]); 7 | } 8 | -------------------------------------------------------------------------------- /mobile/src/store/modules/rootSaga.js: -------------------------------------------------------------------------------- 1 | import { all } from 'redux-saga/effects'; 2 | 3 | import auth from './auth/sagas'; 4 | 5 | export default function* rootSaga() { 6 | return yield all([auth]); 7 | } 8 | -------------------------------------------------------------------------------- /frontend/.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 10 | -------------------------------------------------------------------------------- /backend/src/app/views/emails/partials/header.hbs: -------------------------------------------------------------------------------- 1 |
2 | 6 |
-------------------------------------------------------------------------------- /mobile/src/pages/HelpOrders/NewOrder/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Container = styled.ScrollView` 4 | flex: 1; 5 | background: #f5f5f5; 6 | 7 | padding: 15px 25px 10px 25px; 8 | `; 9 | -------------------------------------------------------------------------------- /mobile/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import { AppRegistry } from 'react-native'; 6 | import App from './src/index'; 7 | import { name as appName } from './app.json'; 8 | 9 | AppRegistry.registerComponent(appName, () => App); 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /mobile/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /mobile/src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import logobar from '~/assets/logobar.png'; 4 | import { Container, Logo } from './styles'; 5 | 6 | export default function Header() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/config/mail.js: -------------------------------------------------------------------------------- 1 | export default { 2 | host: process.env.MAIL_HOST, 3 | port: process.env.MAIL_PORT, 4 | secure: false, 5 | auth: { 6 | user: process.env.MAIL_USER, 7 | pass: process.env.MAIL_PASS, 8 | }, 9 | default: { 10 | from: 'Gympoint Team ', 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /frontend/config-overrides.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | const { addBabelPlugin, override } = require('customize-cra'); 3 | 4 | module.exports = override( 5 | addBabelPlugin([ 6 | 'babel-plugin-root-import', 7 | { 8 | rootPathSuffix: 'src', 9 | }, 10 | ]) 11 | ); 12 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /mobile/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | import createRouter from './routes'; 5 | 6 | export default function App() { 7 | const signed = useSelector(state => state.auth.signed); 8 | 9 | const Routes = createRouter(signed); 10 | 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/services/error.js: -------------------------------------------------------------------------------- 1 | import { toast } from 'react-toastify'; 2 | 3 | export default function throwError(error) { 4 | const message = error.response 5 | ? error.response.data.error 6 | : 'Network Error. Check your connection.'; 7 | 8 | toast.error(`Error: ${message}`, { 9 | autoClose: 5000, 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /mobile/src/components/Header/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Container = styled.View` 4 | background: #dddddd; 5 | width: 100%; 6 | height: 50px; 7 | margin-bottom: 15px; 8 | border: 1px solid #99999999; 9 | `; 10 | 11 | export const Logo = styled.Image` 12 | width: 100px; 13 | `; 14 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | APP_URL= 2 | NODE_ENV= 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= 18 | REDIS_PORT= 19 | 20 | # Mail 21 | 22 | MAIL_HOST= 23 | MAIL_PORT= 24 | MAIL_USER= 25 | MAIL_PASS= 26 | 27 | # Sentry 28 | 29 | SENTRY_DSN= 30 | -------------------------------------------------------------------------------- /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 | }; -------------------------------------------------------------------------------- /backend/src/app/views/emails/helporder.hbs: -------------------------------------------------------------------------------- 1 | Hello, {{ name }} 2 | 3 |

Your question has been answered:

4 | 5 |

{{ question }}

6 | 7 |

Answer: {{ answer }}

8 | 9 |

-

10 |

Count on us! We are available to answer your questions and help you.

11 |

-------------------------------------------------------------------------------- /mobile/src/pages/SignIn/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Container = styled.View` 4 | flex: 1; 5 | background: #f5f5f5; 6 | justify-content: center; 7 | align-items: center; 8 | padding: 0 20px; 9 | `; 10 | 11 | export const Form = styled.View` 12 | align-self: stretch; 13 | margin-top: 10px; 14 | `; 15 | -------------------------------------------------------------------------------- /backend/src/config/database.js: -------------------------------------------------------------------------------- 1 | require('dotenv/config'); 2 | 3 | module.exports = { 4 | dialect: 'postgres', 5 | host: process.env.DB_HOST, 6 | username: process.env.DB_USER, 7 | password: process.env.DB_PASS, 8 | database: process.env.DB_NAME, 9 | define: { 10 | timestamps: true, 11 | underscored: true, 12 | underscoredAll: true, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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'], 10 | }, 11 | reducers 12 | ); 13 | 14 | return persistedReducer; 15 | }; 16 | -------------------------------------------------------------------------------- /mobile/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'mobile' 2 | include ':react-native-gesture-handler' 3 | 4 | project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android') 5 | 6 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 7 | include ':app' 8 | -------------------------------------------------------------------------------- /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: ['auth'], 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/createStore.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { createStore, compose, applyMiddleware } from 'redux'; 3 | 4 | export default (reducers, middlewares) => { 5 | const enhancer = 6 | process.env.NODE_ENV === 'development' 7 | ? compose(console.tron.createEnhancer(), applyMiddleware(...middlewares)) 8 | : applyMiddleware(...middlewares); 9 | 10 | return createStore(reducers, enhancer); 11 | }; 12 | -------------------------------------------------------------------------------- /mobile/src/store/createStore.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { createStore, compose, applyMiddleware } from 'redux'; 3 | 4 | export default (reducers, middlewares) => { 5 | // eslint-disable-next-line no-undef 6 | const enhancer = __DEV__ 7 | ? compose(console.tron.createEnhancer(), applyMiddleware(...middlewares)) 8 | : applyMiddleware(...middlewares); 9 | 10 | return createStore(reducers, enhancer); 11 | }; 12 | -------------------------------------------------------------------------------- /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/src/pages/_layouts/default/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { Wrapper } from './styles'; 5 | import Header from '~/components/Header'; 6 | 7 | export default function DefaultLayout({ children }) { 8 | return ( 9 | 10 |
11 | {children} 12 | 13 | ); 14 | } 15 | 16 | DefaultLayout.propTypes = { 17 | children: PropTypes.element.isRequired, 18 | }; 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/src/config/ReactotronConfig.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import Reactotron from 'reactotron-react-js'; 3 | import { reactotronRedux } from 'reactotron-redux'; 4 | import reactotronSaga from 'reactotron-redux-saga'; 5 | 6 | if (process.env.NODE_ENV === 'development') { 7 | const tron = Reactotron.configure() 8 | .use(reactotronRedux()) 9 | .use(reactotronSaga()) 10 | .connect(); 11 | 12 | tron.clear(); 13 | 14 | console.tron = tron; 15 | } 16 | -------------------------------------------------------------------------------- /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/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: 46px; 6 | background: #ee4e62; 7 | border-radius: 4px; 8 | align-items: center; 9 | justify-content: center; 10 | width: 100%; 11 | `; 12 | 13 | export const Text = styled.Text` 14 | color: #fff; 15 | font-weight: bold; 16 | font-size: 16px; 17 | `; 18 | -------------------------------------------------------------------------------- /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.DECIMAL(10, 2), 10 | canceled_at: Sequelize.DATE, 11 | }, 12 | { 13 | sequelize, 14 | } 15 | ); 16 | 17 | return this; 18 | } 19 | } 20 | 21 | export default Plan; 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/src/app/models/Student.js: -------------------------------------------------------------------------------- 1 | import Sequelize, { Model } from 'sequelize'; 2 | 3 | class Student extends Model { 4 | static init(sequelize) { 5 | super.init( 6 | { 7 | name: Sequelize.STRING, 8 | email: Sequelize.STRING, 9 | age: Sequelize.INTEGER, 10 | weight: Sequelize.DECIMAL(10, 2), 11 | height: Sequelize.DECIMAL(10, 2), 12 | }, 13 | { 14 | sequelize, 15 | } 16 | ); 17 | 18 | return this; 19 | } 20 | } 21 | 22 | export default Student; 23 | -------------------------------------------------------------------------------- /mobile/src/components/Input/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Container = styled.View` 4 | margin: 15px 0px; 5 | padding: 0 10px; 6 | height: 46px; 7 | background: #fff; 8 | border-radius: 4px; 9 | flex-direction: row; 10 | align-items: center; 11 | border: 1px solid #dddddd; 12 | `; 13 | 14 | export const TInput = styled.TextInput.attrs({ 15 | placeholderTextColor: '#999', 16 | })` 17 | flex: 1; 18 | font-size: 15px; 19 | margin-left: 10px; 20 | color: #555; 21 | `; 22 | -------------------------------------------------------------------------------- /backend/src/database/seeds/20191027212532-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/store/modules/auth/action.js: -------------------------------------------------------------------------------- 1 | export function signInRequest(studentID) { 2 | return { 3 | type: '@auth/SIGN_IN_REQUEST', 4 | payload: { studentID }, 5 | }; 6 | } 7 | 8 | export function signInSuccess(id, name) { 9 | return { 10 | type: '@auth/SIGN_IN_SUCCESS', 11 | payload: { id, name }, 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 | -------------------------------------------------------------------------------- /mobile/src/components/MultilineInput/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Container = styled.View` 4 | margin: 15px 0px; 5 | height: 250px; 6 | background: #fff; 7 | border-radius: 4px; 8 | flex-direction: row; 9 | align-items: flex-start; 10 | 11 | border: 1px solid #dddddd; 12 | `; 13 | 14 | export const TInput = styled.TextInput.attrs({ 15 | placeholderTextColor: '#999', 16 | })` 17 | flex: 1; 18 | font-size: 15px; 19 | margin-left: 5px; 20 | color: #555; 21 | height: 200px; 22 | `; 23 | -------------------------------------------------------------------------------- /frontend/src/store/modules/auth/action.js: -------------------------------------------------------------------------------- 1 | export function signInRequest(email, password) { 2 | return { 3 | type: '@auth/SIGN_IN_REQUEST', 4 | payload: { email, password }, 5 | }; 6 | } 7 | 8 | export function signInSuccess(token, user) { 9 | return { 10 | type: '@auth/SIGN_IN_SUCCESS', 11 | payload: { token, user }, 12 | }; 13 | } 14 | 15 | export function signFailure() { 16 | return { 17 | type: '@auth/SIGN_FAILURE', 18 | }; 19 | } 20 | 21 | export function signOut() { 22 | return { 23 | type: '@auth/SIGN_OUT', 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true 5 | }, 6 | extends: ["airbnb-base", "prettier"], 7 | plugins: ["prettier"], 8 | globals: { 9 | Atomics: "readonly", 10 | SharedArrayBuffer: "readonly" 11 | }, 12 | parserOptions: { 13 | ecmaVersion: 2018, 14 | sourceType: "module" 15 | }, 16 | rules: { 17 | "prettier/prettier": "error", 18 | "class-methods-use-this": "off", 19 | "no-param-reassign": "off", 20 | camelcase: "off", 21 | "no-unused-vars": ["error", { argsIgnorePattern: "next" }] 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /backend/src/app/models/HelpOrder.js: -------------------------------------------------------------------------------- 1 | import Sequelize, { Model } from 'sequelize'; 2 | 3 | class HelpOrder 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 HelpOrder; 25 | -------------------------------------------------------------------------------- /mobile/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PersistGate } from 'redux-persist/integration/react'; 3 | import { Provider } from 'react-redux'; 4 | import { StatusBar } from 'react-native'; 5 | 6 | import './config/ReactotronConfig'; 7 | 8 | import { store, persistor } from './store'; 9 | 10 | import App from './App'; 11 | 12 | export default function Index() { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /mobile/src/config/ReactotronConfig.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import Reactotron from 'reactotron-react-native'; 3 | import { reactotronRedux } from 'reactotron-redux'; 4 | import reactotronSaga from 'reactotron-redux-saga'; 5 | import { AsyncStorage } from '@react-native-community/async-storage'; 6 | 7 | // eslint-disable-next-line no-undef 8 | if (__DEV__) { 9 | const tron = Reactotron.setAsyncStorageHandler(AsyncStorage) 10 | .configure() 11 | .useReactNative() 12 | .use(reactotronRedux()) 13 | .use(reactotronSaga()) 14 | .connect(); 15 | 16 | tron.clear(); 17 | 18 | console.tron = tron; 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/app/jobs/HelpOrderAnsweredMail.js: -------------------------------------------------------------------------------- 1 | import Mail from '../../lib/Mail'; 2 | 3 | class HelpOrderAnsweredMail { 4 | get key() { 5 | return 'HelpOrderAnsweredMail'; 6 | } 7 | 8 | async handle({ data }) { 9 | const { student, question, answer } = data; 10 | 11 | await Mail.sendMail({ 12 | to: `${student.name} <${student.email}>`, 13 | subject: 'Your question has been answered!', 14 | template: 'helporder', 15 | context: { 16 | name: student.name, 17 | question, 18 | answer, 19 | }, 20 | }); 21 | } 22 | } 23 | 24 | export default new HelpOrderAnsweredMail(); 25 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | Gympoint 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /backend/src/app/views/emails/enrollment.hbs: -------------------------------------------------------------------------------- 1 | Hello, {{ name }} 2 |

First of all, we would like to welcome you to Gympoint!

3 | 4 |

We know that the decision for a gym involves a lot of factors and we are delighted that you chose our gym center.

5 | 6 |

From your start date you will enjoy our facilities and equipment.

7 |

8 |

9 | Plan: {{ planName }}
10 | Duration: {{ planDuration }} mo
11 | Start date: {{ start_date }}
12 | End date: {{end_date}}
13 | Price: U$ {{totalPrice}} (U$ {{planPrice}}/month)
14 |

-------------------------------------------------------------------------------- /backend/src/app/middleware/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 decoded = await promisify(jwt.verify)(token, authConfig.secret); 17 | 18 | req.userID = decoded.id; 19 | 20 | return next(); 21 | } catch (err) { 22 | return res.status(401).json({ error: 'Token invalid' }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /mobile/src/components/Input/index.js: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Icon from 'react-native-vector-icons/MaterialIcons'; 4 | import { Container, TInput } from './styles'; 5 | 6 | Icon.loadFont(); 7 | 8 | function Input({ style, icon, ...rest }, ref) { 9 | return ( 10 | 11 | {icon && } 12 | 13 | 14 | ); 15 | } 16 | 17 | Input.propTypes = { 18 | icon: PropTypes.string, 19 | style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), 20 | }; 21 | 22 | Input.defaultProps = { 23 | icon: null, 24 | style: {}, 25 | }; 26 | 27 | export default forwardRef(Input); 28 | -------------------------------------------------------------------------------- /mobile/src/store/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { persistStore } from 'redux-persist'; 3 | import createSagaMiddleware from 'redux-saga'; 4 | import createStore from './createStore'; 5 | 6 | import rootReducer from './modules/rootReducer'; 7 | import rootSaga from './modules/rootSaga'; 8 | import persistReducers from './persistReducers'; 9 | 10 | // eslint-disable-next-line no-undef 11 | const sagaMonitor = __DEV__ ? console.tron.createSagaMonitor() : null; 12 | 13 | const sagaMiddleware = createSagaMiddleware({ sagaMonitor }); 14 | 15 | const middlewares = [sagaMiddleware]; 16 | 17 | const store = createStore(persistReducers(rootReducer), middlewares); 18 | const persistor = persistStore(store); 19 | 20 | sagaMiddleware.run(rootSaga); 21 | 22 | export { store, persistor }; 23 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { persistStore } from 'redux-persist'; 3 | import createSagaMiddleware from 'redux-saga'; 4 | import createStore from './createStore'; 5 | 6 | import rootReducer from './modules/rootReducer'; 7 | import rootSaga from './modules/rootSaga'; 8 | import persistReducers from './persistReducers'; 9 | 10 | const sagaMonitor = 11 | process.env.NODE_ENV === 'development' 12 | ? console.tron.createSagaMonitor() 13 | : null; 14 | 15 | const sagaMiddleware = createSagaMiddleware({ sagaMonitor }); 16 | 17 | const middlewares = [sagaMiddleware]; 18 | 19 | const store = createStore(persistReducers(rootReducer), middlewares); 20 | const persistor = persistStore(store); 21 | 22 | sagaMiddleware.run(rootSaga); 23 | 24 | export { store, persistor }; 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/src/database/index.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | import User from '../app/models/User'; 3 | import databaseConfig from '../config/database'; 4 | import Student from '../app/models/Student'; 5 | import Plan from '../app/models/Plan'; 6 | import Enrollment from '../app/models/Enrollment'; 7 | import Checkin from '../app/models/Checkin'; 8 | import HelpOrder from '../app/models/HelpOrder'; 9 | 10 | const models = [User, Student, Plan, Enrollment, Checkin, HelpOrder]; 11 | 12 | class Database { 13 | constructor() { 14 | this.init(); 15 | } 16 | 17 | init() { 18 | this.connection = new Sequelize(databaseConfig); 19 | models.map(model => model.init(this.connection)); 20 | 21 | models.map( 22 | model => model.associate && model.associate(this.connection.models) 23 | ); 24 | } 25 | } 26 | export default new Database(); 27 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PersistGate } from 'redux-persist/integration/react'; 3 | import { Provider } from 'react-redux'; 4 | import { Router } from 'react-router-dom'; 5 | import { ToastContainer } from 'react-toastify'; 6 | 7 | import './config/ReactotronConfig'; 8 | 9 | import Routes from './routes'; 10 | import GlobalStyle from './styles/global'; 11 | 12 | import history from './services/history'; 13 | import { store, persistor } from './store'; 14 | 15 | function App() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | } 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /frontend/src/styles/global.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | import 'react-toastify/dist/ReactToastify.css'; 3 | 4 | export default createGlobalStyle` 5 | @import url('https://fonts.googleapis.com/css?family=Roboto:400,700&display=swap'); 6 | 7 | * { 8 | margin: 0; 9 | padding: 0; 10 | outline: 0; 11 | box-sizing: border-box; 12 | } 13 | 14 | *:focus { 15 | outline: 0; 16 | } 17 | 18 | html, body, #root { 19 | height:100%; 20 | background: #f5f5f5; 21 | 22 | 23 | body { 24 | -webkit-font-smoothing: antialiased; 25 | } 26 | 27 | body, input, button { 28 | font-size: 14px; 29 | font-family: 'Roboto', sans-serif; 30 | } 31 | 32 | a { 33 | text-decoration: none; 34 | } 35 | 36 | ul { 37 | list-style: none; 38 | } 39 | 40 | button { 41 | cursor: pointer; 42 | } 43 | } 44 | `; 45 | -------------------------------------------------------------------------------- /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/20191105002554-create-checkins.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => 3 | 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: { 13 | model: 'students', 14 | key: 'id', 15 | }, 16 | onUpdate: 'CASCADE', 17 | onDelete: 'SET NULL', 18 | allowNull: true, 19 | }, 20 | created_at: { 21 | type: Sequelize.DATE, 22 | allowNull: false, 23 | }, 24 | updated_at: { 25 | type: Sequelize.DATE, 26 | allowNull: false, 27 | }, 28 | }), 29 | 30 | down: queryInterface => queryInterface.dropTable('checkins'), 31 | }; 32 | -------------------------------------------------------------------------------- /backend/src/app/views/emails/layouts/styles.css: -------------------------------------------------------------------------------- 1 | .container { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 16px; 4 | line-height: 1.6; 5 | color: #222; 6 | max-width: 100%; 7 | margin: 0; 8 | padding: 0; 9 | box-sizing: border-box; 10 | } 11 | 12 | header { 13 | display: flex; 14 | background: #ff6b6b; 15 | color: white; 16 | height: 50px; 17 | align-items: center; 18 | } 19 | 20 | header nav { 21 | display: flex; 22 | align-items: center; 23 | justify-content: space-between; 24 | height: 60px; 25 | font-size: 14px; 26 | padding: 0 20px; 27 | } 28 | 29 | nav img { 30 | height: 45px; 31 | } 32 | 33 | footer { 34 | display: flex; 35 | background: #4a90e2; 36 | color: white; 37 | height: 40px; 38 | align-items: center; 39 | padding: 0 20px; 40 | justify-content: space-between; 41 | } 42 | 43 | .content { 44 | background: #f2f2f2; 45 | padding: 20px; 46 | } 47 | -------------------------------------------------------------------------------- /mobile/src/components/MultilineInput/index.js: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Icon from 'react-native-vector-icons/MaterialIcons'; 4 | import { Container, TInput } from './styles'; 5 | 6 | Icon.loadFont(); 7 | 8 | function MultilineInput({ style, icon, ...rest }, ref) { 9 | return ( 10 | 11 | {icon && } 12 | 18 | 19 | ); 20 | } 21 | 22 | MultilineInput.propTypes = { 23 | icon: PropTypes.string, 24 | style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), 25 | }; 26 | 27 | MultilineInput.defaultProps = { 28 | icon: null, 29 | style: {}, 30 | }; 31 | 32 | export default forwardRef(MultilineInput); 33 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20191027213417-create-users.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => 3 | 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 | down: queryInterface => queryInterface.dropTable('users'), 34 | }; 35 | -------------------------------------------------------------------------------- /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/app/controllers/SessionController.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | 3 | import User from '../models/User'; 4 | import authConfig from '../../config/auth'; 5 | 6 | class SessionController { 7 | async store(req, res) { 8 | const { email, password } = req.body; 9 | 10 | const user = await User.findOne({ 11 | where: { email }, 12 | }); 13 | 14 | if (!user) { 15 | return res.status(401).json({ 16 | error: 'User does not exist', 17 | }); 18 | } 19 | 20 | if (!(await user.checkPassword(password))) { 21 | return res.status(401).json({ 22 | error: 'Password does not match', 23 | }); 24 | } 25 | 26 | const { id, name } = user; 27 | 28 | return res.json({ 29 | user: { id, name, email }, 30 | token: jwt.sign({ id }, authConfig.secret, { 31 | expiresIn: authConfig.expiresIn, 32 | }), 33 | }); 34 | } 35 | } 36 | 37 | export default new SessionController(); 38 | -------------------------------------------------------------------------------- /mobile/src/store/modules/auth/reducer.js: -------------------------------------------------------------------------------- 1 | import produce from 'immer'; 2 | 3 | const INITIAL_STATE = { 4 | signed: false, 5 | loading: false, 6 | id: null, 7 | name: null, 8 | }; 9 | 10 | export default function auth(state = INITIAL_STATE, action) { 11 | return produce(state, draft => { 12 | switch (action.type) { 13 | case '@auth/SIGN_IN_REQUEST': { 14 | draft.loading = true; 15 | break; 16 | } 17 | case '@auth/SIGN_IN_SUCCESS': { 18 | draft.id = action.payload.id; 19 | draft.name = action.payload.name; 20 | draft.signed = true; 21 | draft.loading = false; 22 | break; 23 | } 24 | case '@auth/SIGN_FAILURE': { 25 | draft.signed = false; 26 | draft.loading = false; 27 | break; 28 | } 29 | case '@auth/SIGN_OUT': { 30 | draft.token = null; 31 | draft.signed = false; 32 | break; 33 | } 34 | default: 35 | } 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/app/jobs/NewEnrollmentMail.js: -------------------------------------------------------------------------------- 1 | import { format, parseISO } from 'date-fns'; 2 | import Mail from '../../lib/Mail'; 3 | 4 | class NewEnrollmentMail { 5 | get key() { 6 | return 'NewEnrollmentMail'; 7 | } 8 | 9 | async handle({ data }) { 10 | const { studentExist, planExist, start_date, end_date, price } = data; 11 | 12 | await Mail.sendMail({ 13 | to: `${studentExist.name} <${studentExist.email}>`, 14 | subject: 'New Enrollment', 15 | template: 'enrollment', 16 | context: { 17 | name: studentExist.name, 18 | planName: planExist.title, 19 | planPrice: parseFloat(planExist.price).toFixed(2), 20 | planDuration: planExist.duration, 21 | start_date: format(parseISO(start_date), "MMMM' 'dd', 'yyyy"), 22 | end_date: format(parseISO(end_date), "MMMM' 'dd', 'yyyy"), 23 | totalPrice: parseFloat(price).toFixed(2), 24 | }, 25 | }); 26 | } 27 | } 28 | 29 | export default new NewEnrollmentMail(); 30 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20191030000254-create-plans.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => 3 | queryInterface.createTable('plans', { 4 | id: { 5 | type: Sequelize.INTEGER, 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | title: { 11 | type: Sequelize.STRING, 12 | allowNull: false, 13 | }, 14 | duration: { 15 | type: Sequelize.INTEGER, 16 | allowNull: false, 17 | }, 18 | price: { 19 | type: Sequelize.DECIMAL(10, 2), 20 | allowNull: false, 21 | }, 22 | canceled_at: { 23 | type: Sequelize.DATE, 24 | }, 25 | created_at: { 26 | type: Sequelize.DATE, 27 | allowNull: false, 28 | }, 29 | updated_at: { 30 | type: Sequelize.DATE, 31 | allowNull: false, 32 | }, 33 | }), 34 | 35 | down: queryInterface => queryInterface.dropTable('plans'), 36 | }; 37 | -------------------------------------------------------------------------------- /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 | name: null, 8 | }; 9 | 10 | export default function auth(state = INITIAL_STATE, action) { 11 | return produce(state, draft => { 12 | switch (action.type) { 13 | case '@auth/SIGN_IN_REQUEST': { 14 | draft.loading = true; 15 | break; 16 | } 17 | case '@auth/SIGN_IN_SUCCESS': { 18 | draft.token = action.payload.token; 19 | draft.name = action.payload.user.name; 20 | draft.signed = true; 21 | draft.loading = false; 22 | break; 23 | } 24 | case '@auth/SIGN_FAILURE': { 25 | draft.signed = false; 26 | draft.loading = false; 27 | break; 28 | } 29 | case '@auth/SIGN_OUT': { 30 | draft.token = null; 31 | draft.signed = false; 32 | break; 33 | } 34 | default: 35 | } 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 | -------------------------------------------------------------------------------- /mobile/src/pages/Checkin/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Container = styled.View` 4 | flex: 1; 5 | background: #f5f5f5; 6 | justify-content: center; 7 | align-items: center; 8 | padding: 15px 10px 10px 10px; 9 | `; 10 | 11 | export const Empty = styled.View` 12 | justify-content: center; 13 | align-items: center; 14 | `; 15 | 16 | export const TextEmpty = styled.Text` 17 | color: #999; 18 | `; 19 | 20 | export const CheckinList = styled.FlatList` 21 | width: 100%; 22 | margin-top: 15px; 23 | `; 24 | 25 | export const CheckinItem = styled.View` 26 | height: 46px; 27 | display: flex; 28 | flex-direction: row; 29 | justify-content: space-between; 30 | align-items: center; 31 | 32 | padding: 0px 15px; 33 | 34 | margin-top: 15px; 35 | 36 | border-radius: 4px; 37 | border: 1px #dddddd; 38 | background: #ffffff; 39 | `; 40 | 41 | export const Title = styled.Text` 42 | font-weight: bold; 43 | color: #444444; 44 | `; 45 | 46 | export const OccurredAt = styled.Text` 47 | color: #666666; 48 | `; 49 | -------------------------------------------------------------------------------- /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/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /mobile/src/store/modules/auth/sagas.js: -------------------------------------------------------------------------------- 1 | import { takeLatest, call, put, all } from 'redux-saga/effects'; 2 | import { Alert } from 'react-native'; 3 | 4 | import api from '~/services/api'; 5 | 6 | import { signInSuccess, signFailure } from './action'; 7 | 8 | export function* signIn({ payload }) { 9 | try { 10 | const { studentID } = payload; 11 | 12 | if (!studentID) { 13 | Alert.alert('Student ID is required!', 'Please, type a valid ID.'); 14 | throw new Error(); 15 | } 16 | 17 | const response = yield call(api.get, `students/${studentID}`); 18 | 19 | if (response.data) { 20 | const { name, id } = response.data; 21 | 22 | yield put(signInSuccess(id, name)); 23 | } else { 24 | Alert.alert('Student not found!', 'Please, type a valid ID.'); 25 | throw new Error(); 26 | } 27 | } catch (error) { 28 | yield put(signFailure()); 29 | } 30 | } 31 | 32 | export function signOut() { 33 | // history.push('/'); 34 | } 35 | 36 | export default all([ 37 | takeLatest('@auth/SIGN_IN_REQUEST', signIn), 38 | takeLatest('@auth/SIGN_OUT', signOut), 39 | ]); 40 | -------------------------------------------------------------------------------- /mobile/src/pages/HelpOrders/Order/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { 5 | Container, 6 | OrderItem, 7 | OccurredAt, 8 | Question, 9 | Answer, 10 | Header, 11 | Title, 12 | } from './styles'; 13 | 14 | export default function Order(props) { 15 | const { order } = props.navigation.state.params; 16 | 17 | return ( 18 | 19 | {order && ( 20 | 21 |
22 | QUESTION 23 | {order.dateFormatted} 24 |
25 | {order.question} 26 | ANSWER 27 | 28 | {order.answered 29 | ? order.answer 30 | : 'This question has not been answered yet. Come back soon!'} 31 | 32 |
33 | )} 34 |
35 | ); 36 | } 37 | 38 | Order.propTypes = { 39 | navigation: PropTypes.shape({ 40 | navigate: PropTypes.func.isRequired, 41 | }).isRequired, 42 | }; 43 | -------------------------------------------------------------------------------- /backend/src/lib/Queue.js: -------------------------------------------------------------------------------- 1 | import Bee from 'bee-queue'; 2 | import redisConfig from '../config/redis'; 3 | import NewEnrollmentMail from '../app/jobs/NewEnrollmentMail'; 4 | import HelpOrderAnsweredMail from '../app/jobs/HelpOrderAnsweredMail'; 5 | 6 | const jobs = [HelpOrderAnsweredMail, NewEnrollmentMail]; 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 | // eslint-disable-next-line no-console 40 | console.log(`Queue ${job.queue.name}: FAILED`, err); 41 | } 42 | } 43 | 44 | export default new Queue(); 45 | -------------------------------------------------------------------------------- /backend/src/database/seeds/20191030001035-add-plans.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: QueryInterface => { 3 | return QueryInterface.bulkInsert( 4 | 'plans', 5 | [ 6 | { 7 | title: 'Fit', 8 | duration: 1, 9 | price: 129.0, 10 | canceled_at: null, 11 | created_at: new Date(), 12 | updated_at: new Date(), 13 | }, 14 | { 15 | title: 'Health', 16 | duration: 3, 17 | price: 119.0, 18 | canceled_at: null, 19 | created_at: new Date(), 20 | updated_at: new Date(), 21 | }, 22 | { 23 | title: 'Monster', 24 | duration: 6, 25 | price: 99.0, 26 | canceled_at: null, 27 | created_at: new Date(), 28 | updated_at: new Date(), 29 | }, 30 | { 31 | title: 'Titan', 32 | duration: 12, 33 | price: 79.0, 34 | canceled_at: null, 35 | created_at: new Date(), 36 | updated_at: new Date(), 37 | }, 38 | ], 39 | {} 40 | ); 41 | }, 42 | 43 | down: () => {}, 44 | }; 45 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20191029013007-create-students.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => 3 | 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.DECIMAL(10, 2), 25 | allowNull: false, 26 | }, 27 | height: { 28 | type: Sequelize.DECIMAL(10, 2), 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 | down: queryInterface => queryInterface.dropTable('students'), 42 | }; 43 | -------------------------------------------------------------------------------- /frontend/src/routes/Route.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Route, Redirect } from 'react-router-dom'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import AuthLayout from '~/pages/_layouts/auth'; 7 | import DefaultLayout from '~/pages/_layouts/default'; 8 | import { store } from '~/store'; 9 | 10 | export default function RouteWrapper({ 11 | component: Component, 12 | isPrivate = false, 13 | ...rest 14 | }) { 15 | const { signed } = store.getState().auth; 16 | 17 | if (!signed && isPrivate) { 18 | return ; 19 | } 20 | 21 | if (signed && !isPrivate) { 22 | return ; 23 | } 24 | 25 | const Layout = signed ? DefaultLayout : AuthLayout; 26 | 27 | return ( 28 | ( 31 | 32 | 33 | 34 | )} 35 | /> 36 | ); 37 | } 38 | 39 | RouteWrapper.propTypes = { 40 | isPrivate: PropTypes.bool, 41 | component: PropTypes.oneOfType([PropTypes.element, PropTypes.func]) 42 | .isRequired, 43 | }; 44 | 45 | RouteWrapper.defaultProps = { 46 | isPrivate: false, 47 | }; 48 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "nodemon src/server.js", 8 | "queue": "nodemon src/queue.js" 9 | }, 10 | "dependencies": { 11 | "@sentry/node": "5.7.1", 12 | "bcryptjs": "^2.4.3", 13 | "bee-queue": "^1.2.2", 14 | "cors": "^2.8.5", 15 | "date-fns": "^2.6.0", 16 | "dotenv": "^8.2.0", 17 | "express": "^4.17.1", 18 | "express-async-errors": "^3.1.1", 19 | "express-handlebars": "^3.1.0", 20 | "jsonwebtoken": "^8.5.1", 21 | "nodemailer": "^6.3.1", 22 | "nodemailer-express-handlebars": "^3.1.0", 23 | "pg": "^7.12.1", 24 | "pg-hstore": "^2.3.3", 25 | "sequelize": "^5.21.1", 26 | "youch": "^2.0.10", 27 | "yup": "^0.27.0" 28 | }, 29 | "devDependencies": { 30 | "eslint": "^6.6.0", 31 | "eslint-config-airbnb-base": "^14.0.0", 32 | "eslint-config-prettier": "^6.5.0", 33 | "eslint-plugin-import": "^2.18.2", 34 | "eslint-plugin-prettier": "^3.1.1", 35 | "nodemon": "^1.19.4", 36 | "prettier": "^1.18.2", 37 | "sequelize-cli": "^5.5.1", 38 | "sucrase": "^3.10.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20191106000601-create-help-orders.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => 3 | queryInterface.createTable('help_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: { 13 | model: 'students', 14 | key: 'id', 15 | }, 16 | onUpdate: 'CASCADE', 17 | onDelete: 'SET NULL', 18 | allowNull: true, 19 | }, 20 | question: { 21 | type: Sequelize.STRING, 22 | allowNull: false, 23 | }, 24 | answer: { 25 | type: Sequelize.STRING, 26 | allowNull: true, 27 | }, 28 | answer_at: { 29 | type: Sequelize.DATE, 30 | allowNull: true, 31 | }, 32 | created_at: { 33 | type: Sequelize.DATE, 34 | allowNull: false, 35 | }, 36 | updated_at: { 37 | type: Sequelize.DATE, 38 | allowNull: false, 39 | }, 40 | }), 41 | 42 | down: queryInterface => queryInterface.dropTable('help_orders'), 43 | }; 44 | -------------------------------------------------------------------------------- /backend/src/app/models/Enrollment.js: -------------------------------------------------------------------------------- 1 | import Sequelize, { Model } from 'sequelize'; 2 | import { isBefore, isAfter } from 'date-fns'; 3 | 4 | class Enrollment extends Model { 5 | static init(sequelize) { 6 | super.init( 7 | { 8 | start_date: Sequelize.DATE, 9 | canceled_at: Sequelize.DATE, 10 | end_date: { 11 | type: Sequelize.DATE, 12 | }, 13 | price: { 14 | type: Sequelize.DECIMAL(10, 2), 15 | }, 16 | active: { 17 | type: Sequelize.VIRTUAL(Sequelize.BOOLEAN, [ 18 | 'start_date', 19 | 'end_date', 20 | ]), 21 | get() { 22 | return ( 23 | isBefore(this.get('start_date'), new Date()) && 24 | isAfter(this.get('end_date'), new Date()) 25 | ); 26 | }, 27 | }, 28 | }, 29 | { 30 | sequelize, 31 | } 32 | ); 33 | 34 | return this; 35 | } 36 | 37 | static associate(models) { 38 | this.belongsTo(models.Student, { foreignKey: 'student_id', as: 'student' }); 39 | this.belongsTo(models.Plan, { foreignKey: 'plan_id', as: 'plan' }); 40 | } 41 | } 42 | 43 | export default Enrollment; 44 | -------------------------------------------------------------------------------- /mobile/src/pages/SignIn/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | 4 | import { Image } from 'react-native'; 5 | 6 | import Button from '~/components/Button'; 7 | import Input from '~/components/Input'; 8 | import logo from '~/assets/logo.png'; 9 | 10 | import { signInRequest } from '~/store/modules/auth/action'; 11 | 12 | import { Container, Form } from './styles'; 13 | 14 | export default function SignIn() { 15 | const dispatch = useDispatch(); 16 | const [studentID, setStudentID] = useState(null); 17 | 18 | function handleSign() { 19 | dispatch(signInRequest(studentID)); 20 | } 21 | 22 | return ( 23 | 24 | 25 |
26 | 36 | 37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/app/views/emails/layouts/default.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{> header}} 3 |
4 | {{{ body }}} 5 |
6 | 7 | {{> footer}} 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /frontend/src/pages/Dashboard/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { FaRegIdCard } from 'react-icons/fa'; 4 | import { MdAttachMoney, MdQuestionAnswer, MdPeople } from 'react-icons/md'; 5 | 6 | import { Container, Panel } from './styles'; 7 | 8 | export default function Dashboard() { 9 | return ( 10 | 11 | 12 | 13 |
14 | 15 | STUDENTS 16 |
17 | 18 | 19 |
20 | 21 | PLANS 22 |
23 | 24 | 25 | 26 |
27 | 28 | ENROLLMENTS 29 |
30 | 31 | 32 | 33 |
34 | 35 | HELP ORDERS 36 |
37 | 38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /backend/src/app.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import express from 'express'; 3 | import cors from 'cors'; 4 | import * as Sentry from '@sentry/node'; 5 | import 'express-async-errors'; 6 | import Youch from 'youch'; 7 | import routes from './routes'; 8 | 9 | import './database'; 10 | import sentryConfig from './config/sentry'; 11 | 12 | class App { 13 | constructor() { 14 | this.server = express(); 15 | 16 | Sentry.init(sentryConfig); 17 | 18 | this.middlewares(); 19 | this.routes(); 20 | this.exceptionHandler(); 21 | } 22 | 23 | middlewares() { 24 | this.server.use(Sentry.Handlers.requestHandler()); 25 | this.server.use(cors()); 26 | this.server.use(express.json()); 27 | } 28 | 29 | routes() { 30 | this.server.use(routes); 31 | 32 | this.server.use(Sentry.Handlers.errorHandler()); 33 | } 34 | 35 | exceptionHandler() { 36 | this.server.use(async (err, req, res, next) => { 37 | if (process.env.NODE_ENV === 'development') { 38 | const errors = await new Youch(err, req).toJSON(); 39 | 40 | return res.status(500).json(errors); 41 | } 42 | 43 | return res.status(500).json({ error: 'Internal server error' }); 44 | }); 45 | } 46 | } 47 | 48 | export default new App().server; 49 | -------------------------------------------------------------------------------- /backend/src/lib/Mail.js: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | import exphbs from 'express-handlebars'; 3 | import nodemailerhbs from 'nodemailer-express-handlebars'; 4 | import { resolve } from 'path'; 5 | 6 | import mailConfig from '../config/mail'; 7 | 8 | class Mail { 9 | constructor() { 10 | const { host, port, secure, auth } = mailConfig; 11 | 12 | this.transporter = nodemailer.createTransport({ 13 | host, 14 | port, 15 | secure, 16 | auth: auth.user ? auth : null, 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/src/pages/HelpOrders/Order/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Container = styled.ScrollView` 4 | flex: 1; 5 | background: #f5f5f5; 6 | 7 | padding: 15px 10px 10px 10px; 8 | `; 9 | 10 | export const OrderItem = styled.View` 11 | display: flex; 12 | 13 | padding: 20px; 14 | 15 | margin-bottom: 15px; 16 | 17 | border-radius: 4px; 18 | border: 1px #dddddd; 19 | background: #ffffff; 20 | `; 21 | 22 | export const Header = styled.View` 23 | display: flex; 24 | flex-direction: row; 25 | justify-content: space-between; 26 | align-items: center; 27 | `; 28 | 29 | export const Status = styled.Text` 30 | font-weight: bold; 31 | color: #666666; 32 | 33 | ${({ answered }) => 34 | answered && 35 | ` 36 | color: green; 37 | `} 38 | `; 39 | 40 | export const OccurredAt = styled.Text` 41 | color: #666666; 42 | `; 43 | 44 | export const Title = styled.Text` 45 | color: #444; 46 | font-weight: bold; 47 | `; 48 | 49 | export const Answer = styled.Text` 50 | font-size: 14px; 51 | margin-top: 10px; 52 | color: #666666; 53 | line-height: 21px; 54 | 55 | ${({ unanswered }) => unanswered && `font-style: italic; color:#999;`} 56 | `; 57 | 58 | export const Question = styled(Answer)` 59 | margin-bottom: 15px; 60 | `; 61 | -------------------------------------------------------------------------------- /mobile/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | }, 6 | extends: ['airbnb', 'prettier', 'prettier/react'], 7 | globals: { 8 | Atomics: 'readonly', 9 | SharedArrayBuffer: 'readonly', 10 | }, 11 | parser: 'babel-eslint', 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | ecmaVersion: 2018, 17 | sourceType: 'module', 18 | }, 19 | plugins: ['react', 'prettier'], 20 | rules: { 21 | 'prettier/prettier': 'error', 22 | 'react/jsx-filename-extension': [ 23 | 'warn', 24 | { 25 | extensions: ['.jsx', '.js'], 26 | }, 27 | ], 28 | 'import/prefer-default-export': 'off', 29 | 'react/state-in-constructor': 'off', 30 | 'react/destructuring-assignment': 'off', 31 | 'react/static-property-placement': 'off', 32 | 'react/prefer-stateless-function': 'off', 33 | 'no-param-reassign': 'off', 34 | 'react/prop-types': 'off', 35 | 'react/jsx-props-no-spreading': 'off', 36 | 'no-unused-expressions': 'off', 37 | 'no-alert': 'off', 38 | 'jsx-a11y/control-has-associated-label': 'off', 39 | }, 40 | settings: { 41 | 'import/resolver': { 42 | 'babel-plugin-root-import': { 43 | rootPathSuffix: 'src', 44 | }, 45 | }, 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /frontend/src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | 5 | import logo from '~/assets/logo-header.svg'; 6 | 7 | import { Container, Content, Profile } from './styles'; 8 | import { signOut } from '~/store/modules/auth/action'; 9 | 10 | export default function Header() { 11 | const dispatch = useDispatch(); 12 | const { name } = useSelector(state => state.auth); 13 | 14 | function handleSignOut() { 15 | dispatch(signOut()); 16 | } 17 | 18 | return ( 19 | 20 | 21 | 29 | 30 | 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/pages/SignIn/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { Form, Input } from '@rocketseat/unform'; 4 | import * as Yup from 'yup'; 5 | 6 | import { signInRequest } from '~/store/modules/auth/action'; 7 | 8 | import logo from '~/assets/logo.svg'; 9 | 10 | const schema = Yup.object().shape({ 11 | email: Yup.string() 12 | .email('Insert a valid email') 13 | .required('Email is required'), 14 | password: Yup.string().required('Password is required'), 15 | }); 16 | 17 | export default function SignIn() { 18 | const dispatch = useDispatch(); 19 | const loading = useSelector(state => state.auth.loading); 20 | 21 | function handleSubmit({ email, password }) { 22 | dispatch(signInRequest(email, password)); 23 | } 24 | 25 | return ( 26 | <> 27 | GymPoint 28 |
29 |
30 | Your e-mail 31 | 32 |
33 | 34 |
35 | Your password 36 | 37 |
38 | 39 | 40 |
41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/components/Header/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | background: #fff; 5 | padding: 0 30px; 6 | border-bottom: 1px solid #dddddd; 7 | `; 8 | 9 | export const Content = styled.div` 10 | height: 64px; 11 | max-width: 1400px; 12 | margin: 0 auto; 13 | display: flex; 14 | justify-content: space-between; 15 | align-items: center; 16 | 17 | nav { 18 | width: 600px; 19 | display: flex; 20 | align-items: center; 21 | justify-content: space-between; 22 | 23 | img { 24 | margin-right: 20px; 25 | padding-right: 20px; 26 | border-right: 1px solid #eee; 27 | } 28 | 29 | a { 30 | font-weight: bold; 31 | color: #999; 32 | 33 | &:hover { 34 | color: #555; 35 | } 36 | } 37 | } 38 | aside { 39 | display: flex; 40 | align-items: center; 41 | } 42 | `; 43 | 44 | export const Profile = styled.div` 45 | display: flex; 46 | margin-left: 20px; 47 | padding-left: 20px; 48 | border-left: 1px solid #eee; 49 | 50 | div { 51 | text-align: right; 52 | margin-right: 10px; 53 | 54 | strong { 55 | display: block; 56 | color: #555; 57 | } 58 | 59 | a { 60 | display: block; 61 | margin-top: 2px; 62 | font-size: 12px; 63 | color: #ee4d64; 64 | } 65 | } 66 | `; 67 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | }, 6 | extends: ['airbnb', 'prettier', 'prettier/react'], 7 | globals: { 8 | Atomics: 'readonly', 9 | SharedArrayBuffer: 'readonly', 10 | }, 11 | parser: 'babel-eslint', 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | ecmaVersion: 2018, 17 | sourceType: 'module', 18 | }, 19 | plugins: ['react', 'prettier', 'react-hooks'], 20 | rules: { 21 | 'prettier/prettier': 'error', 22 | 'react/jsx-filename-extension': [ 23 | 'warn', 24 | { 25 | extensions: ['.jsx', '.js'], 26 | }, 27 | ], 28 | 'import/prefer-default-export': 'off', 29 | 'react/state-in-constructor': 'off', 30 | 'react/destructuring-assignment': 'off', 31 | 'react/static-property-placement': 'off', 32 | 'react/prefer-stateless-function': 'off', 33 | 'no-param-reassign': 'off', 34 | 'react/prop-types': 'off', 35 | 'react/jsx-props-no-spreading': 'off', 36 | 'no-unused-expressions': 'off', 37 | 'no-alert': 'off', 38 | 'jsx-a11y/control-has-associated-label': 'off', 39 | 40 | 'react-hooks/rules-of-hooks': 'error', 41 | 'react-hooks/exhaustive-deps': 0, 42 | }, 43 | settings: { 44 | 'import/resolver': { 45 | 'babel-plugin-root-import': { 46 | rootPathSuffix: 'src', 47 | }, 48 | }, 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /mobile/src/pages/HelpOrders/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Container = styled.View` 4 | flex: 1; 5 | background: #f5f5f5; 6 | justify-content: center; 7 | align-items: center; 8 | padding: 15px 10px 10px 10px; 9 | `; 10 | 11 | export const OrderList = styled.FlatList` 12 | width: 100%; 13 | margin-top: 10px; 14 | padding-top: 10px; 15 | `; 16 | 17 | export const Empty = styled.View` 18 | justify-content: center; 19 | align-items: center; 20 | `; 21 | 22 | export const TextEmpty = styled.Text` 23 | color: #999; 24 | `; 25 | 26 | export const OrderItem = styled.View` 27 | display: flex; 28 | 29 | padding: 20px; 30 | 31 | margin-bottom: 15px; 32 | 33 | border-radius: 4px; 34 | border: 1px #dddddd; 35 | background: #ffffff; 36 | `; 37 | 38 | export const Header = styled.View` 39 | display: flex; 40 | flex-direction: row; 41 | justify-content: space-between; 42 | align-items: center; 43 | `; 44 | 45 | export const StatusArea = styled.View` 46 | display: flex; 47 | flex-direction: row; 48 | align-items: center; 49 | `; 50 | 51 | export const Status = styled.Text` 52 | padding-left: 5px; 53 | font-weight: bold; 54 | color: #999; 55 | 56 | ${({ answered }) => 57 | answered && 58 | ` 59 | color: green; 60 | `} 61 | `; 62 | 63 | export const OccurredAt = styled.Text` 64 | color: #666666; 65 | `; 66 | 67 | export const Question = styled.Text` 68 | margin-top: 10px; 69 | color: #666666; 70 | line-height: 21px; 71 | `; 72 | -------------------------------------------------------------------------------- /frontend/src/store/modules/auth/sagas.js: -------------------------------------------------------------------------------- 1 | import { takeLatest, call, put, all } from 'redux-saga/effects'; 2 | import { toast } from 'react-toastify'; 3 | import { css } from 'glamor'; 4 | 5 | import history from '~/services/history'; 6 | 7 | import api from '~/services/api'; 8 | 9 | import { signInSuccess, signFailure } from './action'; 10 | 11 | export function* signIn({ payload }) { 12 | try { 13 | const { email, password } = payload; 14 | 15 | const response = yield call(api.post, 'sessions', { 16 | email, 17 | password, 18 | }); 19 | 20 | const { token, user } = response.data; 21 | 22 | api.defaults.headers.Authorization = `Bearer ${token}`; 23 | 24 | yield put(signInSuccess(token, user)); 25 | 26 | history.push('/dashboard'); 27 | } catch (error) { 28 | toast('Authentication failure. Check your e-mail and password.', { 29 | className: css({ 30 | background: '#f1f1f !important', 31 | color: '#ee4d64 !important', 32 | }), 33 | }); 34 | yield put(signFailure()); 35 | } 36 | } 37 | 38 | export function signOut() { 39 | history.push('/'); 40 | } 41 | 42 | export function setToken({ payload }) { 43 | if (!payload) return; 44 | 45 | const { token } = payload.auth; 46 | 47 | if (token) { 48 | api.defaults.headers.Authorization = `Bearer ${token}`; 49 | } 50 | } 51 | 52 | export default all([ 53 | takeLatest('persist/REHYDRATE', setToken), 54 | takeLatest('@auth/SIGN_IN_REQUEST', signIn), 55 | takeLatest('@auth/SIGN_OUT', signOut), 56 | ]); 57 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20191031012441-create-enrollment.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => 3 | queryInterface.createTable('enrollments', { 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: { 13 | model: 'students', 14 | key: 'id', 15 | }, 16 | onUpdate: 'CASCADE', 17 | onDelete: 'SET NULL', 18 | allowNull: true, 19 | }, 20 | plan_id: { 21 | type: Sequelize.INTEGER, 22 | references: { 23 | model: 'plans', 24 | key: 'id', 25 | }, 26 | onUpdate: 'CASCADE', 27 | onDelete: 'SET NULL', 28 | allowNull: true, 29 | }, 30 | start_date: { 31 | type: Sequelize.DATE, 32 | allowNull: false, 33 | }, 34 | end_date: { 35 | type: Sequelize.DATE, 36 | allowNull: false, 37 | }, 38 | price: { 39 | type: Sequelize.DECIMAL(10, 2), 40 | allowNull: false, 41 | }, 42 | canceled_at: { 43 | type: Sequelize.DATE, 44 | }, 45 | created_at: { 46 | type: Sequelize.DATE, 47 | allowNull: false, 48 | }, 49 | updated_at: { 50 | type: Sequelize.DATE, 51 | allowNull: false, 52 | }, 53 | }), 54 | 55 | down: queryInterface => queryInterface.dropTable('enrollments'), 56 | }; 57 | -------------------------------------------------------------------------------- /mobile/android/app/BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") 12 | 13 | lib_deps = [] 14 | 15 | create_aar_targets(glob(["libs/*.aar"])) 16 | 17 | create_jar_targets(glob(["libs/*.jar"])) 18 | 19 | android_library( 20 | name = "all-libs", 21 | exported_deps = lib_deps, 22 | ) 23 | 24 | android_library( 25 | name = "app-code", 26 | srcs = glob([ 27 | "src/main/java/**/*.java", 28 | ]), 29 | deps = [ 30 | ":all-libs", 31 | ":build_config", 32 | ":res", 33 | ], 34 | ) 35 | 36 | android_build_config( 37 | name = "build_config", 38 | package = "com.mobile", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "com.mobile", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /mobile/ios/mobile/AppDelegate.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 "AppDelegate.h" 9 | 10 | #import 11 | #import 12 | #import 13 | 14 | @implementation AppDelegate 15 | 16 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 17 | { 18 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 19 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge 20 | moduleName:@"mobile" 21 | initialProperties:nil]; 22 | 23 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 24 | 25 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 26 | UIViewController *rootViewController = [UIViewController new]; 27 | rootViewController.view = rootView; 28 | self.window.rootViewController = rootViewController; 29 | [self.window makeKeyAndVisible]; 30 | return YES; 31 | } 32 | 33 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 34 | { 35 | #if DEBUG 36 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 37 | #else 38 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 39 | #endif 40 | } 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /frontend/src/pages/Dashboard/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { darken, lighten } from 'polished'; 3 | 4 | export const Container = styled.div` 5 | @import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap'); 6 | 7 | max-width: 1200px; 8 | margin: 30px auto; 9 | display: flex; 10 | flex-direction: column; 11 | `; 12 | 13 | export const Panel = styled.div` 14 | width: 100%; 15 | padding: 45px; 16 | background: #fff; 17 | border-radius: 5px; 18 | 19 | display: flex; 20 | flex-wrap: wrap; 21 | justify-content: center; 22 | 23 | div { 24 | /* background: #ee4d64; 25 | width: 150px; 26 | height: 100px; 27 | border-radius: 20px; 28 | padding: 35px; 29 | font-weight: bold; 30 | display: flex; 31 | flex-direction: column; 32 | align-items: center; 33 | font-size: 18px; 34 | color: #f2f2f2; */ 35 | 36 | display: flex; 37 | width: 500px; 38 | 39 | justify-content: center; 40 | align-items: center; 41 | background: ${lighten(0.02, '#EE4D64')}; 42 | border-radius: 8px; 43 | transition: all 0.2s ease 0s; 44 | padding: 80px 50px; 45 | margin: 20px; 46 | font-size: 30px; 47 | color: #f2f2f2; 48 | box-shadow: 3px 3px 3px 3px rgba(0, 0, 0, 0.3); 49 | cursor: pointer; 50 | font-family: 'Open Sans', sans-serif; 51 | 52 | span { 53 | margin-left: 30px; 54 | font-weight: bold; 55 | } 56 | 57 | &:hover { 58 | background: ${darken(0.05, lighten(0.02, '#EE4D64'))}; 59 | transform: translateY(-7px); 60 | } 61 | } 62 | `; 63 | -------------------------------------------------------------------------------- /backend/src/app/controllers/CheckinController.js: -------------------------------------------------------------------------------- 1 | import { Op } from 'sequelize'; 2 | import { subDays } from 'date-fns'; 3 | 4 | import Checkin from '../models/Checkin'; 5 | import Student from '../models/Student'; 6 | 7 | class CheckinController { 8 | async index(req, res) { 9 | const checkins = await Checkin.findAll({ 10 | where: { 11 | student_id: req.params.studentId, 12 | }, 13 | order: [['created_at', 'DESC']], 14 | attributes: ['id', 'created_at'], 15 | include: [ 16 | { 17 | model: Student, 18 | as: 'student', 19 | attributes: ['id', 'name', 'age'], 20 | }, 21 | ], 22 | }); 23 | 24 | return res.json(checkins); 25 | } 26 | 27 | async store(req, res) { 28 | const { studentId: student_id } = req.params; 29 | 30 | const studentExist = await Student.findByPk(student_id); 31 | 32 | if (!studentExist) { 33 | return res.status(400).json({ error: 'Student not found' }); 34 | } 35 | 36 | const startWeek = subDays(new Date(), 7); 37 | 38 | const amountCheckins = await Checkin.findAndCountAll({ 39 | where: { 40 | student_id, 41 | createdAt: { 42 | [Op.gte]: startWeek, 43 | }, 44 | }, 45 | }); 46 | 47 | if (amountCheckins.count >= 5) { 48 | return res 49 | .status(400) 50 | .json({ error: 'You can check in only 5 times within 7 days' }); 51 | } 52 | 53 | const { id, createdAt } = await Checkin.create({ 54 | student_id, 55 | }); 56 | 57 | return res.json({ 58 | id, 59 | createdAt, 60 | }); 61 | } 62 | } 63 | 64 | export default new CheckinController(); 65 | -------------------------------------------------------------------------------- /mobile/ios/mobile-tvOS/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSExceptionDomains 28 | 29 | localhost 30 | 31 | NSExceptionAllowsInsecureHTTPLoads 32 | 33 | 34 | 35 | 36 | NSLocationWhenInUseUsageDescription 37 | 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIRequiredDeviceCapabilities 41 | 42 | armv7 43 | 44 | UISupportedInterfaceOrientations 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UIViewControllerBasedStatusBarAppearance 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /mobile/src/pages/HelpOrders/NewOrder/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Alert } from 'react-native'; 3 | 4 | import { useSelector } from 'react-redux'; 5 | import { Container } from './styles'; 6 | import MultilineInput from '~/components/MultilineInput'; 7 | import Button from '~/components/Button'; 8 | import api from '~/services/api'; 9 | 10 | export default function NewOrder(props) { 11 | const userID = useSelector(state => state.auth.id); 12 | const [order, setOrder] = useState(''); 13 | 14 | async function handleBack() { 15 | const { refreshItems } = props.navigation.state.params; 16 | if (typeof refreshItems === 'function') { 17 | await refreshItems(); 18 | 19 | props.navigation.goBack(); 20 | } 21 | } 22 | 23 | async function handleSubmit() { 24 | await api 25 | .post(`/students/${userID}/help-orders`, { question: order }) 26 | .then(() => { 27 | Alert.alert('Sent!', 'Soon your questions will be answered!', [ 28 | { text: 'OK', onPress: () => handleBack() }, 29 | ]); 30 | }) 31 | .catch(error => { 32 | const message = error.response 33 | ? { 34 | title: 'Order error!', 35 | subtitle: error.response.data.error, 36 | } 37 | : { 38 | title: 'Network error!', 39 | subtitle: 'Check your connection.', 40 | }; 41 | 42 | Alert.alert(message.title, message.subtitle); 43 | }); 44 | } 45 | 46 | return ( 47 | 48 | 54 | 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch } from 'react-router-dom'; 3 | 4 | import SignIn from '~/pages/SignIn'; 5 | import Dashboard from '~/pages/Dashboard'; 6 | import Students from '~/pages/Students'; 7 | import Plans from '~/pages/Plans'; 8 | import Enrollments from '~/pages/Enrollments'; 9 | import HelpOrders from '~/pages/HelpOrders'; 10 | import Route from './Route'; 11 | 12 | import FormStudent from '~/pages/Students/FormStudent'; 13 | import FormPlan from '~/pages/Plans/FormPlan'; 14 | import FormEnrollment from '~/pages/Enrollments/FormEnrollment'; 15 | 16 | export default function Routes() { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 42 | 48 | 49 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/pages/Plans/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { darken } from 'polished'; 3 | 4 | export const Container = styled.div` 5 | max-width: 800px; 6 | margin: 50px auto; 7 | display: flex; 8 | flex-direction: column; 9 | `; 10 | 11 | export const TitleBar = styled.div` 12 | display: flex; 13 | justify-content: space-between; 14 | margin: 20px 0px; 15 | 16 | button { 17 | width: 120px; 18 | height: 30px; 19 | background: #ee4d64; 20 | font-weight: bold; 21 | color: #fff; 22 | border: 0; 23 | border-radius: 4px; 24 | font-size: 16px; 25 | transition: background 0.2s; 26 | padding: 0px 10px; 27 | display: flex; 28 | align-items: center; 29 | justify-content: space-around; 30 | 31 | &:hover { 32 | background: ${darken(0.03, '#EE4D64')}; 33 | } 34 | } 35 | `; 36 | 37 | export const PlanTable = styled.table` 38 | width: 100%; 39 | padding: 15px; 40 | background: #fff; 41 | border-radius: 5px; 42 | 43 | thead th { 44 | font-size: 16px; 45 | color: #333; 46 | text-align: left; 47 | padding: 12px 0px; 48 | } 49 | 50 | tbody td { 51 | font-size: 16px; 52 | border-bottom: 1px solid #eee; 53 | color: #555; 54 | padding: 12px 0px; 55 | 56 | div { 57 | width: 45%; 58 | display: flex; 59 | justify-content: space-between; 60 | margin-left: 50px; 61 | } 62 | 63 | :nth-child(1) { 64 | width: 40%; 65 | } 66 | 67 | :nth-child(4) { 68 | width: 20%; 69 | } 70 | } 71 | 72 | tbody center { 73 | color: #888; 74 | } 75 | 76 | button { 77 | width: 30px; 78 | height: 30px; 79 | background: #ee4d64; 80 | color: #fff; 81 | border: 0; 82 | border-radius: 4px; 83 | transition: background 0.2s; 84 | 85 | &:hover { 86 | background: ${darken(0.03, '#EE4D64')}; 87 | } 88 | } 89 | `; 90 | -------------------------------------------------------------------------------- /mobile/README.md: -------------------------------------------------------------------------------- 1 | ## Gympoint - Mobile (Android only) 2 | 3 |

4 | Gympoint 5 |

6 | 7 |

8 | License 9 |

10 | 11 | > This project is being built for the final challenge of Rocketseat's GoStack bootcamp. 12 | 13 | > Gym management using Javascript stacks: node.js (backend), ReactJS (frontend) and React Native (mobile). 14 | 15 | > The mobile version was developed for Android only. 16 | 17 | ## Requirements 18 | - [x] Front-end mobile: [Challenge 10](https://github.com/Rocketseat/bootcamp-gostack-desafio-10) 19 | 20 | ## Technologies 21 | 22 | This project was developed using the following libs and technologies: 23 | 24 | ### Mobile 25 | 26 | - [x] [React Native](https://facebook.github.io/react-native/) 27 | - [x] [React Navigation](https://reactnavigation.org/) 28 | - [x] [Redux](https://redux.js.org/) 29 | - [x] [Redux Saga](https://github.com/redux-saga/redux-saga) 30 | - [x] [Styled Components](https://www.styled-components.com/) 31 | - [x] [Redux Persist](https://github.com/rt2zz/redux-persist) 32 | - [x] [Reactotron](https://github.com/infinitered/reactotron) 33 | - [x] [Axios](https://github.com/axios/axios) 34 | - [x] [ESLint](https://eslint.org/) 35 | - [x] [Prettier](https://prettier.io/) 36 | 37 | ## Installation 38 | 39 | ### Mobile 40 | ```sh 41 | # install all dependencies 42 | yarn install 43 | 44 | # start the app on a new terminal window 45 | react-native run-android 46 | react-native start 47 | ``` 48 | 49 | ## Author 50 | 51 | 👤 **Nathan Ribeiro** 52 | 53 | * Linkedin: [Nathan Ribeiro](https://www.linkedin.com/in/nathanfribeiro/) 54 | * Github: [@NathanFRibeiro](https://github.com/NathanFRibeiro) 55 | 56 | ## Show your support 57 | 58 | Give a ⭐️ if you like this project! 59 | 60 | *** 61 | _Made with ❤️ by [Nathan Ribeiro](https://github.com/NathanFRibeiro)_ 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional REPL history 58 | .node_repl_history 59 | 60 | # Output of 'npm pack' 61 | *.tgz 62 | 63 | # Yarn Integrity file 64 | .yarn-integrity 65 | 66 | # dotenv environment variables file 67 | .env 68 | .env.test 69 | 70 | # parcel-bundler cache (https://parceljs.org/) 71 | .cache 72 | 73 | # next.js build output 74 | .next 75 | 76 | # nuxt.js build output 77 | .nuxt 78 | 79 | # vuepress build output 80 | .vuepress/dist 81 | 82 | # Serverless directories 83 | .serverless/ 84 | 85 | # FuseBox cache 86 | .fusebox/ 87 | 88 | # DynamoDB Local files 89 | .dynamodb/ 90 | 91 | ### VisualStudioCode ### 92 | .vscode/* 93 | !.vscode/settings.json 94 | !.vscode/tasks.json 95 | !.vscode/launch.json 96 | !.vscode/extensions.json 97 | 98 | ### VisualStudioCode Patch ### 99 | # Ignore all local history of files 100 | .history 101 | -------------------------------------------------------------------------------- /frontend/src/pages/Enrollments/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { darken } from 'polished'; 3 | 4 | export const Container = styled.div` 5 | max-width: 1200px; 6 | margin: 50px auto; 7 | display: flex; 8 | flex-direction: column; 9 | `; 10 | 11 | export const TitleBar = styled.div` 12 | display: flex; 13 | justify-content: space-between; 14 | margin: 20px 0px; 15 | 16 | button { 17 | width: 150px; 18 | height: 30px; 19 | background: #ee4d64; 20 | font-weight: bold; 21 | color: #fff; 22 | border: 0; 23 | border-radius: 4px; 24 | font-size: 16px; 25 | transition: background 0.2s; 26 | padding: 0px 10px; 27 | display: flex; 28 | align-items: center; 29 | justify-content: space-around; 30 | 31 | &:hover { 32 | background: ${darken(0.03, '#EE4D64')}; 33 | } 34 | } 35 | `; 36 | 37 | export const EnrollmentTable = styled.table` 38 | width: 100%; 39 | height: 100%; 40 | padding: 15px; 41 | background: #fff; 42 | border-radius: 5px; 43 | 44 | thead th { 45 | font-size: 16px; 46 | color: #333; 47 | text-align: left; 48 | padding: 12px 0px; 49 | 50 | :nth-child(5) { 51 | text-align: center; 52 | } 53 | } 54 | 55 | tbody td { 56 | font-size: 16px; 57 | border-bottom: 1px solid #eee; 58 | color: #555; 59 | padding: 12px 0px; 60 | 61 | div { 62 | width: 40%; 63 | display: flex; 64 | justify-content: space-between; 65 | margin-left: 50px; 66 | } 67 | 68 | :nth-child(1) { 69 | width: 25%; 70 | } 71 | 72 | :nth-child(5) { 73 | text-align: center; 74 | } 75 | 76 | :nth-child(6) { 77 | width: 15%; 78 | } 79 | } 80 | 81 | tbody center { 82 | color: #888; 83 | } 84 | 85 | button { 86 | width: 30px; 87 | height: 30px; 88 | background: #ee4d64; 89 | color: #fff; 90 | border: 0; 91 | border-radius: 4px; 92 | transition: background 0.2s; 93 | 94 | &:hover { 95 | background: ${darken(0.03, '#EE4D64')}; 96 | } 97 | } 98 | `; 99 | -------------------------------------------------------------------------------- /frontend/src/pages/_layouts/auth/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { darken } from 'polished'; 3 | 4 | export const Wrapper = styled.div` 5 | height: 100%; 6 | min-height: 100%; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | 11 | background: #ee4d64; 12 | `; 13 | 14 | export const Content = styled.div` 15 | width: 100%; 16 | max-width: 450px; 17 | text-align: center; 18 | 19 | background: #fff; 20 | border-radius: 4px; 21 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); 22 | padding: 30px; 23 | margin: 80px auto; 24 | 25 | form { 26 | display: flex; 27 | flex-direction: column; 28 | margin-top: 30px; 29 | 30 | div { 31 | display: flex; 32 | flex-flow: column; 33 | align-items: flex-start; 34 | padding: 5px 0px; 35 | 36 | strong { 37 | text-transform: uppercase; 38 | color: ${darken(0.5, '#EE4D64')}; 39 | margin-bottom: 5px; 40 | } 41 | 42 | input { 43 | width: 100%; 44 | background: #ffff; 45 | border: 1px solid #ccc; 46 | border-radius: 4px; 47 | height: 44px; 48 | padding: 0 15px; 49 | color: #222; 50 | margin: 0 0 10px; 51 | 52 | &::placeholder { 53 | color: rgba(180, 180, 180); 54 | } 55 | } 56 | } 57 | 58 | span { 59 | color: #fb6f91; 60 | align-self: flex-start; 61 | margin: 0 0 10px; 62 | font-weight: bold; 63 | } 64 | 65 | button { 66 | margin: 5px 0 0; 67 | height: 44px; 68 | background: #ee4d64; 69 | font-weight: bold; 70 | color: #fff; 71 | border: 0; 72 | border-radius: 4px; 73 | font-size: 16px; 74 | transition: background 0.2s; 75 | 76 | &:hover { 77 | background: ${darken(0.03, '#EE4D64')}; 78 | } 79 | } 80 | 81 | a { 82 | color: #fff; 83 | margin-top: 15px; 84 | font-size: 16px; 85 | opacity: 0.8; 86 | 87 | &:hover { 88 | opacity: 1; 89 | } 90 | } 91 | } 92 | `; 93 | -------------------------------------------------------------------------------- /mobile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobile", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios", 8 | "start": "react-native start", 9 | "lint": "eslint ." 10 | }, 11 | "dependencies": { 12 | "@react-native-community/async-storage": "^1.6.3", 13 | "axios": "^0.19.0", 14 | "date-fns": "^2.8.1", 15 | "date-fns-tz": "^1.0.8", 16 | "immer": "^5.0.0", 17 | "prop-types": "^15.7.2", 18 | "react": "16.9.0", 19 | "react-native": "0.61.4", 20 | "react-native-gesture-handler": "^1.5.0", 21 | "react-native-reanimated": "^1.4.0", 22 | "react-native-screens": "^1.0.0-alpha.23", 23 | "react-native-vector-icons": "^6.6.0", 24 | "react-navigation": "^4.0.10", 25 | "react-navigation-stack": "^1.10.3", 26 | "react-navigation-tabs": "^2.6.0", 27 | "react-redux": "^7.1.3", 28 | "reactotron-react-native": "^4.0.2", 29 | "reactotron-redux": "^3.1.2", 30 | "reactotron-redux-saga": "^4.2.3", 31 | "redux": "^4.0.4", 32 | "redux-persist": "^6.0.0", 33 | "redux-saga": "^1.1.3", 34 | "styled-components": "^4.4.1" 35 | }, 36 | "devDependencies": { 37 | "@babel/core": "^7.7.2", 38 | "@babel/runtime": "^7.7.2", 39 | "@react-native-community/eslint-config": "^0.0.5", 40 | "babel-eslint": "^10.0.3", 41 | "babel-jest": "^24.9.0", 42 | "babel-plugin-root-import": "^6.4.1", 43 | "eslint": "^6.6.0", 44 | "eslint-config-airbnb": "^18.0.1", 45 | "eslint-config-prettier": "^6.7.0", 46 | "eslint-import-resolver-babel-plugin-root-import": "^1.1.1", 47 | "eslint-plugin-import": "^2.18.2", 48 | "eslint-plugin-jsx-a11y": "^6.2.3", 49 | "eslint-plugin-prettier": "^3.1.1", 50 | "eslint-plugin-react": "^7.16.0", 51 | "eslint-plugin-react-hooks": "^1.7.0", 52 | "jest": "^24.9.0", 53 | "jetifier": "^1.6.4", 54 | "metro-react-native-babel-preset": "^0.57.0", 55 | "prettier": "^1.19.1", 56 | "react-test-renderer": "16.9.0" 57 | }, 58 | "jest": { 59 | "preset": "react-native" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 |

2 | Gympoint 3 |

4 | 5 |

6 | License 7 |

8 | 9 | > Gym management using Javascript stacks: node.js (backend), ReactJS (frontend) and React Native (mobile). 10 | 11 | > This project is being built for the final challenge of Rocketseat's GoStack bootcamp. 12 | 13 | ## Requirements 14 | 15 | - [x] [Challenge 09](https://github.com/Rocketseat/bootcamp-gostack-desafio-09) 16 | 17 | ## Technologies 18 | 19 | This project was developed using the following libs and technologies: 20 | 21 | ### Front-end web 22 | 23 | - [x] [React](http://reactjs.org) 24 | - [x] [Redux](https://redux.js.org/) 25 | - [x] [Redux Saga](https://github.com/redux-saga/redux-saga) 26 | - [x] [React Router DOM](https://www.npmjs.com/package/react-router-dom) 27 | - [x] [Styled Components](https://www.styled-components.com/) 28 | - [x] [Redux Persist](https://github.com/rt2zz/redux-persist) 29 | - [x] [Reactotron](https://github.com/infinitered/reactotron) 30 | - [x] [Axios](https://github.com/axios/axios) 31 | - [x] [date-fns](https://date-fns.org/) 32 | - [x] [Polished](https://github.com/styled-components/polished) 33 | - [x] [React-Toastify](https://github.com/fkhadra/react-toastify) 34 | - [x] [@Rocketseat/Unform](https://github.com/Rocketseat/unform) 35 | - [x] [ESLint](https://eslint.org/) 36 | - [x] [Prettier](https://prettier.io/) 37 | 38 | ## Instalation 39 | 40 | ### Frontend Web 41 | 42 | ```sh 43 | # install all dependencies 44 | yarn install 45 | 46 | # start the app on a new terminal window 47 | yarn start 48 | ``` 49 | 50 | ## Author 51 | 52 | 👤 **Nathan Ribeiro** 53 | 54 | - Linkedin: [Nathan Ribeiro](https://www.linkedin.com/in/nathanfribeiro/) 55 | - Github: [@NathanFRibeiro](https://github.com/NathanFRibeiro) 56 | 57 | ## Show your support 58 | 59 | Give a ⭐️ if you like this project! 60 | 61 | --- 62 | 63 | _Made with ❤️ by [Nathan Ribeiro](https://github.com/NathanFRibeiro)_ 64 | -------------------------------------------------------------------------------- /mobile/src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createAppContainer, createSwitchNavigator } from 'react-navigation'; 3 | import { createBottomTabNavigator } from 'react-navigation-tabs'; 4 | import { createStackNavigator } from 'react-navigation-stack'; 5 | import { Image } from 'react-native'; 6 | import Icon from 'react-native-vector-icons/MaterialIcons'; 7 | 8 | import SignIn from './pages/SignIn'; 9 | import Checkin from './pages/Checkin'; 10 | import HelpOrders from './pages/HelpOrders'; 11 | import Order from './pages/HelpOrders/Order'; 12 | import NewOrder from './pages/HelpOrders/NewOrder'; 13 | 14 | import logo from './assets/logobar.png'; 15 | 16 | const stackConfig = { 17 | headerLayoutPreset: 'center', 18 | defaultNavigationOptions: { 19 | headerTitle: , 20 | }, 21 | }; 22 | 23 | const CheckinStack = createStackNavigator( 24 | { 25 | Checkin, 26 | }, 27 | stackConfig 28 | ); 29 | 30 | CheckinStack.navigationOptions = { 31 | tabBarLabel: 'Check-ins', 32 | tabBarIcon: ({ tintColor }) => ( 33 | 34 | ), 35 | }; 36 | 37 | const OrderStack = createStackNavigator( 38 | { 39 | HelpOrders, 40 | Order, 41 | NewOrder, 42 | }, 43 | stackConfig 44 | ); 45 | 46 | OrderStack.navigationOptions = { 47 | tabBarLabel: 'Help Orders', 48 | tabBarIcon: ({ tintColor }) => ( 49 | 50 | ), 51 | }; 52 | 53 | const AppTab = createBottomTabNavigator( 54 | { 55 | Checkin: CheckinStack, 56 | HelpOrders: OrderStack, 57 | }, 58 | { 59 | tabBarOptions: { 60 | activeTintColor: '#ee4e62', 61 | inactiveTintColor: '#999', 62 | style: { 63 | backgroundColor: '#FFFFFF', 64 | borderTopColor: '#DDDDDD', 65 | marginBottom: 10, 66 | }, 67 | }, 68 | } 69 | ); 70 | 71 | export default (signed = false) => 72 | createAppContainer( 73 | createSwitchNavigator( 74 | { 75 | Sign: SignIn, 76 | App: AppTab, 77 | }, 78 | { 79 | initialRouteName: signed ? 'App' : 'Sign', 80 | } 81 | ) 82 | ); 83 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@rocketseat/unform": "^1.6.1", 7 | "axios": "^0.19.0", 8 | "bootstrap": "^4.3.1", 9 | "date-fns": "^2.7.0", 10 | "date-fns-tz": "^1.0.8", 11 | "glamor": "^2.20.40", 12 | "history": "^4.10.1", 13 | "immer": "^5.0.0", 14 | "polished": "^3.4.2", 15 | "prop-types": "^15.7.2", 16 | "react": "^16.11.0", 17 | "react-dom": "^16.11.0", 18 | "react-icons": "^3.8.0", 19 | "react-modal": "^3.11.1", 20 | "react-redux": "^7.1.3", 21 | "react-router-dom": "^5.1.2", 22 | "react-scripts": "3.2.0", 23 | "react-select": "^3.0.8", 24 | "react-toastify": "^5.4.1", 25 | "reactotron-react-js": "^3.3.6", 26 | "reactotron-redux": "^3.1.2", 27 | "reactotron-redux-saga": "^4.2.2", 28 | "redux": "^4.0.4", 29 | "redux-persist": "^6.0.0", 30 | "redux-saga": "^1.1.3", 31 | "styled-components": "^4.4.1", 32 | "yup": "^0.27.0" 33 | }, 34 | "scripts": { 35 | "start": "react-app-rewired start", 36 | "build": "react-app-rewired build", 37 | "test": "react-app-rewired test", 38 | "eject": "react-scripts eject" 39 | }, 40 | "eslintConfig": { 41 | "extends": "react-app" 42 | }, 43 | "browserslist": { 44 | "production": [ 45 | ">0.2%", 46 | "not dead", 47 | "not op_mini all" 48 | ], 49 | "development": [ 50 | "last 1 chrome version", 51 | "last 1 firefox version", 52 | "last 1 safari version" 53 | ] 54 | }, 55 | "devDependencies": { 56 | "babel-eslint": "^10.0.3", 57 | "babel-plugin-root-import": "^6.4.1", 58 | "customize-cra": "^0.8.0", 59 | "eslint": "^6.6.0", 60 | "eslint-config-airbnb": "^18.0.1", 61 | "eslint-config-prettier": "^6.5.0", 62 | "eslint-import-resolver-babel-plugin-root-import": "^1.1.1", 63 | "eslint-plugin-import": "^2.18.2", 64 | "eslint-plugin-jsx-a11y": "^6.2.3", 65 | "eslint-plugin-prettier": "^3.1.1", 66 | "eslint-plugin-react": "^7.16.0", 67 | "eslint-plugin-react-hooks": "^2.3.0", 68 | "prettier": "^1.19.1", 69 | "react-app-rewired": "^2.1.5" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /mobile/ios/mobileTests/mobileTests.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 | #import 10 | 11 | #import 12 | #import 13 | 14 | #define TIMEOUT_SECONDS 600 15 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 16 | 17 | @interface mobileTests : XCTestCase 18 | 19 | @end 20 | 21 | @implementation mobileTests 22 | 23 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 24 | { 25 | if (test(view)) { 26 | return YES; 27 | } 28 | for (UIView *subview in [view subviews]) { 29 | if ([self findSubviewInView:subview matching:test]) { 30 | return YES; 31 | } 32 | } 33 | return NO; 34 | } 35 | 36 | - (void)testRendersWelcomeScreen 37 | { 38 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 39 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 40 | BOOL foundElement = NO; 41 | 42 | __block NSString *redboxError = nil; 43 | #ifdef DEBUG 44 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 45 | if (level >= RCTLogLevelError) { 46 | redboxError = message; 47 | } 48 | }); 49 | #endif 50 | 51 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 52 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 53 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 54 | 55 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 56 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 57 | return YES; 58 | } 59 | return NO; 60 | }]; 61 | } 62 | 63 | #ifdef DEBUG 64 | RCTSetLogFunction(RCTDefaultLogFunction); 65 | #endif 66 | 67 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 68 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 69 | } 70 | 71 | 72 | @end 73 | -------------------------------------------------------------------------------- /mobile/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore polyfills 9 | node_modules/react-native/Libraries/polyfills/.* 10 | 11 | ; These should not be required directly 12 | ; require from fbjs/lib instead: require('fbjs/lib/warning') 13 | node_modules/warning/.* 14 | 15 | ; Flow doesn't support platforms 16 | .*/Libraries/Utilities/LoadingView.js 17 | 18 | [untyped] 19 | .*/node_modules/@react-native-community/cli/.*/.* 20 | 21 | [include] 22 | 23 | [libs] 24 | node_modules/react-native/Libraries/react-native/react-native-interface.js 25 | node_modules/react-native/flow/ 26 | 27 | [options] 28 | emoji=true 29 | 30 | esproposal.optional_chaining=enable 31 | esproposal.nullish_coalescing=enable 32 | 33 | module.file_ext=.js 34 | module.file_ext=.json 35 | module.file_ext=.ios.js 36 | 37 | munge_underscores=true 38 | 39 | module.name_mapper='^react-native$' -> '/node_modules/react-native/Libraries/react-native/react-native-implementation' 40 | module.name_mapper='^react-native/\(.*\)$' -> '/node_modules/react-native/\1' 41 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '/node_modules/react-native/Libraries/Image/RelativeImageStub' 42 | 43 | suppress_type=$FlowIssue 44 | suppress_type=$FlowFixMe 45 | suppress_type=$FlowFixMeProps 46 | suppress_type=$FlowFixMeState 47 | 48 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\) 49 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+ 50 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 51 | 52 | [lints] 53 | sketchy-null-number=warn 54 | sketchy-null-mixed=warn 55 | sketchy-number=warn 56 | untyped-type-import=warn 57 | nonstrict-import=warn 58 | deprecated-type=warn 59 | unsafe-getters-setters=warn 60 | inexact-spread=warn 61 | unnecessary-invariant=warn 62 | signature-verification-failure=warn 63 | deprecated-utility=error 64 | 65 | [strict] 66 | deprecated-type 67 | nonstrict-import 68 | sketchy-null 69 | unclear-type 70 | unsafe-getters-setters 71 | untyped-import 72 | untyped-type-import 73 | 74 | [version] 75 | ^0.105.0 76 | -------------------------------------------------------------------------------- /backend/src/routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import UserController from './app/controllers/UserController'; 4 | import SessionController from './app/controllers/SessionController'; 5 | import StudentController from './app/controllers/StudentController'; 6 | import PlanController from './app/controllers/PlanController'; 7 | import EnrollmentController from './app/controllers/EnrollmentController'; 8 | import CheckinController from './app/controllers/CheckinController'; 9 | import HelpOrderController from './app/controllers/HelpOrderController'; 10 | 11 | import auth from './app/middleware/auth'; 12 | 13 | const routes = new Router(); 14 | 15 | /** 16 | * Sessions 17 | */ 18 | routes.post('/sessions', SessionController.store); 19 | 20 | /** 21 | * Student 22 | */ 23 | routes.get('/students/:id', StudentController.index); 24 | 25 | /** 26 | * Checkins 27 | */ 28 | routes.post('/students/:studentId/checkins', CheckinController.store); 29 | routes.get('/students/:studentId/checkins', CheckinController.index); 30 | 31 | /** 32 | * Help Orders 33 | */ 34 | routes.post('/students/:studentId/help-orders', HelpOrderController.store); 35 | routes.get('/students/:studentId/help-orders', HelpOrderController.index); 36 | 37 | /** 38 | * Authentication Middleware 39 | */ 40 | routes.use(auth); 41 | 42 | /** 43 | * Users 44 | */ 45 | routes.post('/users', UserController.store); 46 | routes.put('/user', UserController.update); 47 | 48 | /** 49 | * Students 50 | */ 51 | routes.get('/students', StudentController.index); 52 | routes.post('/students', StudentController.store); 53 | routes.put('/students/:id', StudentController.update); 54 | 55 | /** 56 | * Plans 57 | */ 58 | routes.post('/plans', PlanController.store); 59 | routes.get('/plans', PlanController.index); 60 | routes.put('/plans/:id', PlanController.update); 61 | routes.delete('/plans/:id', PlanController.delete); 62 | 63 | /** 64 | * Enrollments 65 | */ 66 | routes.post('/enrollment', EnrollmentController.store); 67 | routes.get('/enrollment', EnrollmentController.index); 68 | routes.get('/enrollment/:id', EnrollmentController.index); 69 | routes.put('/enrollment/:id', EnrollmentController.update); 70 | routes.delete('/enrollment/:id', EnrollmentController.delete); 71 | 72 | /** 73 | * Help Orders 74 | */ 75 | routes.get('/help-orders/', HelpOrderController.index); 76 | routes.put('/help-orders/:orderId', HelpOrderController.update); 77 | 78 | export default routes; 79 | -------------------------------------------------------------------------------- /frontend/src/pages/Students/FormStudent/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { darken } from 'polished'; 3 | 4 | export const Container = styled.div` 5 | max-width: 1200px; 6 | margin: 50px auto; 7 | display: flex; 8 | flex-direction: column; 9 | 10 | form { 11 | display: flex; 12 | flex-direction: column; 13 | background-color: #fff; 14 | padding: 40px 30px; 15 | border-radius: 5px; 16 | 17 | div { 18 | margin: 0px 0 10px; 19 | } 20 | 21 | input { 22 | width: 100%; 23 | background: #ffff; 24 | border: 1px solid #cccc; 25 | border-radius: 4px; 26 | height: 44px; 27 | padding: 0 15px; 28 | color: #222; 29 | font-size: 16px; 30 | margin-top: 10px; 31 | 32 | &::placeholder { 33 | color: #9999; 34 | } 35 | } 36 | 37 | span { 38 | color: #fb6f91; 39 | align-self: flex-start; 40 | font-weight: bold; 41 | } 42 | 43 | strong { 44 | text-transform: uppercase; 45 | } 46 | 47 | button { 48 | margin: 5px 0 0; 49 | height: 44px; 50 | background: #ee4d64; 51 | font-weight: bold; 52 | color: #fff; 53 | border: 0; 54 | border-radius: 4px; 55 | font-size: 16px; 56 | transition: background 0.2s; 57 | 58 | &:hover { 59 | background: ${darken(0.03, '#ee4d64')}; 60 | } 61 | } 62 | 63 | a { 64 | color: #fff; 65 | margin-top: 15px; 66 | font-size: 16px; 67 | opacity: 0.8; 68 | 69 | &:hover { 70 | opacity: 1; 71 | } 72 | } 73 | } 74 | `; 75 | 76 | export const HorizontalInputs = styled.div` 77 | display: flex; 78 | width: 100%; 79 | justify-content: space-between; 80 | 81 | div { 82 | width: 350px; 83 | margin-bottom: 10px; 84 | } 85 | `; 86 | 87 | export const TitleBar = styled.div` 88 | display: flex; 89 | justify-content: space-between; 90 | margin: 20px 0px; 91 | 92 | button { 93 | width: 80px; 94 | height: 30px; 95 | background: #ee4d64; 96 | font-weight: bold; 97 | color: #fff; 98 | border: 0; 99 | border-radius: 4px; 100 | font-size: 16px; 101 | transition: background 0.2s; 102 | 103 | &:hover { 104 | background: ${darken(0.03, '#EE4D64')}; 105 | } 106 | } 107 | `; 108 | -------------------------------------------------------------------------------- /backend/src/app/controllers/PlanController.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | import Plan from '../models/Plan'; 3 | 4 | class PlanController { 5 | async index(req, res) { 6 | const plan = await Plan.findAll({ 7 | where: { 8 | canceled_at: null, 9 | }, 10 | order: ['duration'], 11 | }); 12 | 13 | return res.json(plan); 14 | } 15 | 16 | async store(req, res) { 17 | const schema = Yup.object().shape({ 18 | title: Yup.string().required(), 19 | duration: Yup.number().required(), 20 | price: Yup.number().required(), 21 | }); 22 | 23 | if (!(await schema.isValid(req.body))) { 24 | return res.status(400).json({ 25 | error: 'Validation fails', 26 | }); 27 | } 28 | 29 | const planExists = await Plan.findOne({ 30 | where: { title: req.body.title, canceled_at: null }, 31 | }); 32 | 33 | if (planExists) { 34 | return res.status(400).json({ error: 'Plan already exists' }); 35 | } 36 | 37 | const { title, duration, price } = await Plan.create(req.body); 38 | 39 | return res.json({ 40 | title, 41 | duration, 42 | price, 43 | }); 44 | } 45 | 46 | async update(req, res) { 47 | const schema = Yup.object().shape({ 48 | title: Yup.string().required(), 49 | duration: Yup.number(), 50 | price: Yup.number(), 51 | }); 52 | 53 | if (!(await schema.isValid(req.body))) { 54 | return res.status(400).json({ 55 | error: 'Validation fails', 56 | }); 57 | } 58 | 59 | const { id: id_plan } = req.params; 60 | 61 | const plan = await Plan.findByPk(id_plan); 62 | 63 | const { title } = req.body; 64 | 65 | if (title !== Plan.title) { 66 | const planExists = await Plan.findOne({ 67 | where: { title, canceled_at: null }, 68 | }); 69 | 70 | if (planExists) { 71 | return res.status(400).json({ 72 | error: 'Plan already exists.', 73 | }); 74 | } 75 | } 76 | 77 | const { duration, price } = await plan.update(req.body); 78 | 79 | return res.json({ 80 | title, 81 | duration, 82 | price, 83 | }); 84 | } 85 | 86 | async delete(req, res) { 87 | const plan = await Plan.findByPk(req.params.id); 88 | 89 | plan.canceled_at = new Date(); 90 | 91 | await plan.save(); 92 | 93 | return res.json(plan); 94 | } 95 | } 96 | 97 | export default new PlanController(); 98 | -------------------------------------------------------------------------------- /backend/src/app/controllers/UserController.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | import User from '../models/User'; 3 | 4 | class UserController { 5 | async store(req, res) { 6 | const schema = Yup.object().shape({ 7 | name: Yup.string().required(), 8 | email: Yup.string() 9 | .email() 10 | .required(), 11 | password: Yup.string() 12 | .required() 13 | .min(6), 14 | }); 15 | 16 | if (!(await schema.isValid(req.body))) { 17 | return res.status(400).json({ 18 | error: 'Validation fails', 19 | }); 20 | } 21 | 22 | const userExists = await User.findOne({ where: { email: req.body.email } }); 23 | 24 | if (userExists) { 25 | return res.status(400).json({ error: 'User already exists' }); 26 | } 27 | 28 | const { id, name, email, password_hash } = await User.create(req.body); 29 | 30 | return res.json({ 31 | id, 32 | name, 33 | email, 34 | password_hash, 35 | }); 36 | } 37 | 38 | async update(req, res) { 39 | const schema = Yup.object().shape({ 40 | name: Yup.string(), 41 | email: Yup.string().email(), 42 | oldPassword: Yup.string().min(6), 43 | password: Yup.string() 44 | .min(6) 45 | .when('oldPassword', (oldPassword, field) => 46 | oldPassword ? field.required() : field 47 | ), 48 | 49 | confirmPassword: Yup.string().when('password', (password, field) => 50 | password ? field.required().oneOf([Yup.ref('password')]) : field 51 | ), 52 | }); 53 | 54 | if (!(await schema.isValid(req.body))) { 55 | return res.status(400).json({ 56 | error: 'Validation fails', 57 | }); 58 | } 59 | 60 | const user = await User.findByPk(req.userID); 61 | 62 | const { email, oldPassword } = req.body; 63 | 64 | if (email !== user.email) { 65 | const userExists = await User.findOne({ 66 | where: { email }, 67 | }); 68 | 69 | if (userExists) { 70 | return res.status(400).json({ 71 | error: 'User already exists.', 72 | }); 73 | } 74 | } 75 | 76 | if (oldPassword && !(await user.checkPassword(oldPassword))) { 77 | return res.status(401).json({ erro: 'Password does not match' }); 78 | } 79 | 80 | const { id, name } = await user.update(req.body); 81 | 82 | return res.json({ 83 | id, 84 | name, 85 | email, 86 | }); 87 | } 88 | } 89 | 90 | export default new UserController(); 91 | -------------------------------------------------------------------------------- /frontend/src/pages/Students/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { darken } from 'polished'; 3 | 4 | export const Container = styled.div` 5 | max-width: 1200px; 6 | margin: 50px auto; 7 | display: flex; 8 | flex-direction: column; 9 | `; 10 | 11 | export const TitleBar = styled.div` 12 | display: flex; 13 | justify-content: space-between; 14 | margin: 20px 0px; 15 | 16 | aside { 17 | width: 430px; 18 | display: flex; 19 | justify-content: space-between; 20 | 21 | input { 22 | width: 300px; 23 | background: #ffff; 24 | border: 1px solid #cccc; 25 | border-radius: 4px; 26 | height: 30px; 27 | padding: 0 15px; 28 | color: #222; 29 | margin: 0 0 10px; 30 | 31 | &::placeholder { 32 | color: rgba(180, 180, 180); 33 | } 34 | } 35 | 36 | button { 37 | width: 120px; 38 | height: 30px; 39 | background: #ee4d64; 40 | font-weight: bold; 41 | color: #fff; 42 | border: 0; 43 | border-radius: 4px; 44 | font-size: 16px; 45 | transition: background 0.2s; 46 | padding: 0px 10px; 47 | display: flex; 48 | align-items: center; 49 | justify-content: space-around; 50 | 51 | &:hover { 52 | background: ${darken(0.03, '#EE4D64')}; 53 | } 54 | } 55 | } 56 | `; 57 | 58 | export const StudentTable = styled.table` 59 | width: 100%; 60 | padding: 15px; 61 | background: #fff; 62 | border-radius: 5px; 63 | 64 | thead th { 65 | font-size: 16px; 66 | color: #333; 67 | text-align: left; 68 | padding: 12px 0px; 69 | } 70 | 71 | tbody td { 72 | font-size: 16px; 73 | border-bottom: 1px solid #eee; 74 | color: #555; 75 | padding: 12px 0px; 76 | 77 | div { 78 | width: 40%; 79 | display: flex; 80 | justify-content: space-between; 81 | margin-left: 50px; 82 | } 83 | 84 | :nth-child(1) { 85 | width: 40%; 86 | } 87 | 88 | :nth-child(4) { 89 | width: 15%; 90 | } 91 | } 92 | 93 | tbody center { 94 | color: #888; 95 | } 96 | 97 | button { 98 | width: 30px; 99 | height: 30px; 100 | background: #ee4d64; 101 | color: #fff; 102 | border: 0; 103 | border-radius: 4px; 104 | transition: background 0.2s; 105 | 106 | &:hover { 107 | background: ${darken(0.03, '#EE4D64')}; 108 | } 109 | } 110 | `; 111 | -------------------------------------------------------------------------------- /mobile/ios/mobile/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | mobile 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | NSExceptionDomains 32 | 33 | localhost 34 | 35 | NSExceptionAllowsInsecureHTTPLoads 36 | 37 | 38 | 39 | 40 | NSLocationWhenInUseUsageDescription 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UIViewControllerBasedStatusBarAppearance 55 | 56 | UIAppFonts 57 | 58 | AntDesign.ttf 59 | Entypo.ttf 60 | EvilIcons.ttf 61 | Feather.ttf 62 | FontAwesome.ttf 63 | FontAwesome5_Brands.ttf 64 | FontAwesome5_Regular.ttf 65 | FontAwesome5_Solid.ttf 66 | Fontisto.ttf 67 | Foundation.ttf 68 | Ionicons.ttf 69 | MaterialCommunityIcons.ttf 70 | MaterialIcons.ttf 71 | Octicons.ttf 72 | SimpleLineIcons.ttf 73 | Zocial.ttf 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /frontend/src/pages/Plans/FormPlan/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { darken } from 'polished'; 3 | 4 | export const Container = styled.div` 5 | max-width: 1200px; 6 | margin: 50px auto; 7 | display: flex; 8 | flex-direction: column; 9 | 10 | form { 11 | display: flex; 12 | flex-direction: column; 13 | background-color: #fff; 14 | padding: 40px 30px; 15 | border-radius: 5px; 16 | 17 | div { 18 | margin: 0px 0 10px; 19 | } 20 | 21 | input { 22 | width: 100%; 23 | background: #ffff; 24 | border: 1px solid #cccc; 25 | border-radius: 4px; 26 | height: 44px; 27 | padding: 0 15px; 28 | color: #555; 29 | margin-top: 10px; 30 | 31 | &::placeholder { 32 | color: #9999; 33 | } 34 | 35 | &:disabled { 36 | background: #eee; 37 | 38 | &:hover { 39 | cursor: not-allowed; 40 | } 41 | } 42 | } 43 | 44 | span { 45 | color: #fb6f91; 46 | align-self: flex-start; 47 | font-weight: bold; 48 | } 49 | 50 | strong { 51 | text-transform: uppercase; 52 | } 53 | 54 | button { 55 | margin: 5px 0 0; 56 | height: 44px; 57 | background: #ee4d64; 58 | font-weight: bold; 59 | color: #fff; 60 | border: 0; 61 | border-radius: 4px; 62 | font-size: 16px; 63 | transition: background 0.2s; 64 | 65 | &:hover { 66 | background: ${darken(0.03, '#ee4d64')}; 67 | } 68 | } 69 | 70 | a { 71 | color: #fff; 72 | margin-top: 15px; 73 | font-size: 16px; 74 | opacity: 0.8; 75 | 76 | &:hover { 77 | opacity: 1; 78 | } 79 | } 80 | } 81 | `; 82 | 83 | export const HorizontalInputs = styled.div` 84 | display: flex; 85 | width: 100%; 86 | justify-content: space-between; 87 | 88 | div { 89 | width: 350px; 90 | margin-bottom: 10px; 91 | } 92 | `; 93 | 94 | export const TitleBar = styled.div` 95 | display: flex; 96 | justify-content: space-between; 97 | margin: 20px 0px; 98 | 99 | button { 100 | width: 80px; 101 | height: 30px; 102 | background: #ee4d64; 103 | font-weight: bold; 104 | color: #fff; 105 | border: 0; 106 | border-radius: 4px; 107 | font-size: 16px; 108 | transition: background 0.2s; 109 | 110 | &:hover { 111 | background: ${darken(0.03, '#EE4D64')}; 112 | } 113 | } 114 | `; 115 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/java/com/mobile/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.mobile; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import com.facebook.react.PackageList; 6 | import com.facebook.react.ReactApplication; 7 | import com.swmansion.gesturehandler.react.RNGestureHandlerPackage; 8 | import com.facebook.react.ReactNativeHost; 9 | import com.facebook.react.ReactPackage; 10 | import com.facebook.soloader.SoLoader; 11 | import java.lang.reflect.InvocationTargetException; 12 | import java.util.List; 13 | 14 | public class MainApplication extends Application implements ReactApplication { 15 | 16 | private final ReactNativeHost mReactNativeHost = 17 | new ReactNativeHost(this) { 18 | @Override 19 | public boolean getUseDeveloperSupport() { 20 | return BuildConfig.DEBUG; 21 | } 22 | 23 | @Override 24 | protected List getPackages() { 25 | @SuppressWarnings("UnnecessaryLocalVariable") 26 | List packages = new PackageList(this).getPackages(); 27 | // Packages that cannot be autolinked yet can be added manually here, for example: 28 | // packages.add(new MyReactNativePackage()); 29 | return packages; 30 | } 31 | 32 | @Override 33 | protected String getJSMainModuleName() { 34 | return "index"; 35 | } 36 | }; 37 | 38 | @Override 39 | public ReactNativeHost getReactNativeHost() { 40 | return mReactNativeHost; 41 | } 42 | 43 | @Override 44 | public void onCreate() { 45 | super.onCreate(); 46 | SoLoader.init(this, /* native exopackage */ false); 47 | initializeFlipper(this); // Remove this line if you don't want Flipper enabled 48 | } 49 | 50 | /** 51 | * Loads Flipper in React Native templates. 52 | * 53 | * @param context 54 | */ 55 | private static void initializeFlipper(Context context) { 56 | if (BuildConfig.DEBUG) { 57 | try { 58 | /* 59 | We use reflection here to pick up the class that initializes Flipper, 60 | since Flipper library is not available in release mode 61 | */ 62 | Class aClass = Class.forName("com.facebook.flipper.ReactNativeFlipper"); 63 | aClass.getMethod("initializeFlipper", Context.class).invoke(null, context); 64 | } catch (ClassNotFoundException e) { 65 | e.printStackTrace(); 66 | } catch (NoSuchMethodException e) { 67 | e.printStackTrace(); 68 | } catch (IllegalAccessException e) { 69 | e.printStackTrace(); 70 | } catch (InvocationTargetException e) { 71 | e.printStackTrace(); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | ## Gympoint - Backend 2 | 3 |

4 | Gympoint 5 |

6 | 7 |

8 | License 9 |

10 | 11 | 12 | > Gym management using Javascript stacks: node.js (backend), ReactJS (frontend) and React Native (mobile). 13 | 14 | > This project is being built for the final challenge of Rocketseat's GoStack bootcamp. 15 | 16 | ## Requirements 17 | - [x] Back-end: [Challenge 02](https://github.com/Rocketseat/bootcamp-gostack-desafio-02) 18 | - [x] Back-end: [Challenge 03](https://github.com/Rocketseat/bootcamp-gostack-desafio-03) 19 | 20 | ## Technologies 21 | 22 | This project was developed using the following libs and technologies: 23 | 24 | ### Back-end 25 | 26 | - [x] [Node.js](https://nodejs.org/en/) 27 | - [x] [Express](https://expressjs.com/) 28 | - [x] [Postgres](https://www.postgresql.org/) 29 | - [x] [Nodemailer](https://nodemailer.com/) 30 | - [x] [Sequelize](https://sequelize.org/) 31 | - [x] [Handlebars](https://handlebarsjs.com/) 32 | - [x] [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) 33 | - [x] [Nodemon](https://nodemon.io/) 34 | - [x] [Sucrase](https://github.com/alangpierce/sucrase) 35 | - [x] [date-fns](https://date-fns.org/) 36 | - [x] [Bee Queue](https://github.com/bee-queue/bee-queue) 37 | - [x] [Sentry](https://sentry.io/) 38 | - [x] [Youch](https://www.npmjs.com/package/youch) 39 | - [x] [Yup](https://github.com/jquense/yup) 40 | - [x] [ESLint](https://eslint.org/) 41 | - [x] [Prettier](https://prettier.io/) 42 | 43 | 44 | 45 | ## Installation 46 | 47 | ```sh 48 | # create and start two images on docker: database (postgres) and redis. 49 | docker run --name database -e POSTGRES_PASSWORD=password -p 5432:5432 -d postgres 50 | docker run --name redisbase -p 6379:6379 -d -t redis:alpine 51 | docker start database redisbase 52 | ``` 53 | ### Backend 54 | ```sh 55 | # install all dependencies 56 | yarn install 57 | 58 | # execute sequelize migrations and seeds 59 | yarn sequelize db:migrate 60 | yarn sequelize db:seed:all 61 | 62 | # start the e-mail queue service 63 | yarn queue 64 | 65 | # start the app on a new terminal window 66 | yarn dev 67 | ``` 68 | 69 | 70 | ## Author 71 | 72 | 👤 **Nathan Ribeiro** 73 | 74 | * Linkedin: [Nathan Ribeiro](https://www.linkedin.com/in/nathanfribeiro/) 75 | * Github: [@NathanFRibeiro](https://github.com/NathanFRibeiro) 76 | 77 | ## Show your support 78 | 79 | Give a ⭐️ if you like this project! 80 | 81 | *** 82 | _Made with ❤️ by [Nathan Ribeiro](https://github.com/NathanFRibeiro)_ 83 | -------------------------------------------------------------------------------- /frontend/src/pages/Students/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Link, useHistory } from 'react-router-dom'; 3 | import { FaPlus, FaEdit } from 'react-icons/fa'; 4 | 5 | import { Container, TitleBar, StudentTable } from './styles'; 6 | 7 | import api from '~/services/api'; 8 | import throwError from '~/services/error'; 9 | 10 | export default function Students() { 11 | const [students, setStudents] = useState([]); 12 | 13 | const history = useHistory(); 14 | 15 | function handleEdit(studentID) { 16 | history.push(`/students/${studentID}`); 17 | } 18 | 19 | useEffect(() => { 20 | async function loadStudents() { 21 | await api 22 | .get('students/') 23 | .then(response => { 24 | setStudents(response.data); 25 | }) 26 | .catch(error => { 27 | throwError(error); 28 | }); 29 | } 30 | 31 | loadStudents(); 32 | }, []); 33 | 34 | async function handleChange(e) { 35 | if (e.key === 'Enter') { 36 | await api 37 | .get(`students/?name=${e.target.value}`) 38 | .then(response => { 39 | setStudents(response.data); 40 | }) 41 | .catch(error => { 42 | throwError(error); 43 | }); 44 | } 45 | } 46 | 47 | return ( 48 | 49 | 50 |

Student Management

51 | 63 |
64 | 65 | 66 | 67 | NAME 68 | E-MAIL 69 | AGE 70 | 71 | 72 | 73 | 74 | {students.length === 0 ? ( 75 |
No students registered.
76 | ) : ( 77 | students.map(student => ( 78 | 79 | {student.name} 80 | {student.email} 81 | {student.age} 82 | 83 |
84 | 90 |
91 | 92 | 93 | )) 94 | )} 95 | 96 |
97 |
98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /backend/src/app/controllers/StudentController.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | import { Op } from 'sequelize'; 3 | import Student from '../models/Student'; 4 | 5 | class StudentController { 6 | async index(req, res) { 7 | const { name } = req.query; 8 | const { id } = req.params; 9 | 10 | if (id) { 11 | const student = await Student.findByPk(id); 12 | return res.json(student); 13 | } 14 | const student = name 15 | ? await Student.findAll({ 16 | where: { 17 | name: { 18 | [Op.like]: name, 19 | }, 20 | }, 21 | }) 22 | : await Student.findAll({ order: ['id'] }); 23 | 24 | return res.json(student); 25 | } 26 | 27 | async store(req, res) { 28 | const schema = Yup.object().shape({ 29 | name: Yup.string().required(), 30 | email: Yup.string() 31 | .email() 32 | .required(), 33 | age: Yup.number().required(), 34 | weight: Yup.number().required(), 35 | height: Yup.number().required(), 36 | }); 37 | 38 | if (!(await schema.isValid(req.body))) { 39 | return res.status(400).json({ 40 | error: 'Validation fails', 41 | }); 42 | } 43 | 44 | const studentExists = await Student.findOne({ 45 | where: { email: req.body.email }, 46 | }); 47 | 48 | if (studentExists) { 49 | return res.status(400).json({ error: 'Student already exists' }); 50 | } 51 | 52 | const { id, name, email, age, weight, height } = await Student.create( 53 | req.body 54 | ); 55 | 56 | return res.json({ 57 | id, 58 | name, 59 | email, 60 | age, 61 | weight, 62 | height, 63 | }); 64 | } 65 | 66 | async update(req, res) { 67 | const schema = Yup.object().shape({ 68 | name: Yup.string(), 69 | email: Yup.string() 70 | .email() 71 | .required(), 72 | age: Yup.number(), 73 | weight: Yup.number(), 74 | height: Yup.number(), 75 | }); 76 | 77 | if (!(await schema.isValid(req.body))) { 78 | return res.status(400).json({ 79 | error: 'Validation fails', 80 | }); 81 | } 82 | 83 | const { id: id_student } = req.params; 84 | const student = await Student.findByPk(id_student); 85 | 86 | const { email } = req.body; 87 | 88 | if (email !== student.email) { 89 | const studentExists = await Student.findOne({ 90 | where: { email }, 91 | }); 92 | 93 | if (studentExists) { 94 | return res.status(400).json({ 95 | error: 'Student already exists.', 96 | }); 97 | } 98 | } 99 | 100 | const { id, name, age, weight, height } = await student.update(req.body); 101 | 102 | return res.json({ 103 | id, 104 | email, 105 | name, 106 | age, 107 | weight, 108 | height, 109 | }); 110 | } 111 | } 112 | 113 | export default new StudentController(); 114 | -------------------------------------------------------------------------------- /frontend/src/pages/HelpOrders/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { darken } from 'polished'; 3 | 4 | export const Container = styled.div` 5 | max-width: 550px; 6 | margin: 50px auto; 7 | display: flex; 8 | flex-direction: column; 9 | `; 10 | 11 | export const TitleBar = styled.div` 12 | display: flex; 13 | justify-content: space-between; 14 | margin: 20px 0px; 15 | `; 16 | 17 | export const OrdersTable = styled.table` 18 | width: 100%; 19 | padding: 20px 25px; 20 | background: #fff; 21 | border-radius: 5px; 22 | 23 | thead th { 24 | font-size: 16px; 25 | color: #333; 26 | text-align: left; 27 | padding: 12px 0px; 28 | } 29 | 30 | tbody td { 31 | font-size: 16px; 32 | border-bottom: 1px solid #eee; 33 | color: #555; 34 | padding: 12px 0px; 35 | 36 | :nth-child(1) { 37 | width: 90%; 38 | } 39 | } 40 | 41 | tbody center { 42 | color: #888; 43 | } 44 | 45 | button { 46 | width: 30px; 47 | height: 30px; 48 | background: #ee4d64; 49 | color: #fff; 50 | border: 0; 51 | border-radius: 4px; 52 | transition: background 0.2s; 53 | 54 | &:hover { 55 | background: ${darken(0.03, '#EE4D64')}; 56 | } 57 | } 58 | `; 59 | 60 | export const ContentModal = styled.div` 61 | display: flex; 62 | flex-direction: column; 63 | font-size: 14px; 64 | height: 100%; 65 | align-items: space-between; 66 | overflow: hidden; 67 | 68 | div { 69 | display: flex; 70 | flex-direction: column; 71 | font-size: 14px; 72 | 73 | :nth-child(2) { 74 | margin-top: auto; 75 | } 76 | 77 | strong { 78 | color: #333; 79 | margin-bottom: 5px; 80 | } 81 | 82 | p { 83 | color: #f1f1f1; 84 | margin-top: 10px; 85 | margin-left: 7px; 86 | background: #ee4d64; 87 | padding: 10px 15px; 88 | border-radius: 4px; 89 | max-height: 110px; 90 | overflow-y: auto; 91 | } 92 | 93 | p:before { 94 | content: ' '; 95 | position: absolute; 96 | width: 0; 97 | height: 0; 98 | left: 19px; 99 | right: auto; 100 | top: 51px; 101 | bottom: auto; 102 | border: 15px solid; 103 | border-color: #ee4d64 transparent transparent transparent; 104 | } 105 | 106 | span { 107 | font-weight: bold; 108 | color: #ee4d64; 109 | } 110 | 111 | textarea { 112 | height: 140px; 113 | background: #ffff; 114 | border: 1px solid #cccc; 115 | border-radius: 4px; 116 | padding: 5px; 117 | color: #333; 118 | } 119 | 120 | button { 121 | margin: 5px 0 0; 122 | height: 30px; 123 | background: #ee4d64; 124 | font-weight: bold; 125 | color: #fff; 126 | border: 0; 127 | border-radius: 4px; 128 | transition: background 0.2s; 129 | 130 | &:hover { 131 | background: ${darken(0.03, '#ee4d64')}; 132 | } 133 | } 134 | } 135 | `; 136 | -------------------------------------------------------------------------------- /mobile/src/pages/Checkin/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { Alert } from 'react-native'; 4 | 5 | import { parseISO, formatRelative } from 'date-fns'; 6 | 7 | import { 8 | Container, 9 | CheckinList, 10 | CheckinItem, 11 | Title, 12 | OccurredAt, 13 | Empty, 14 | TextEmpty, 15 | } from './styles'; 16 | 17 | import Button from '~/components/Button'; 18 | import api from '~/services/api'; 19 | 20 | export default function Checkin() { 21 | const userID = useSelector(state => state.auth.id); 22 | const [checkins, setCheckins] = useState([]); 23 | 24 | async function configureData(data) { 25 | const checkinList = await data.reverse().map((checkin, idx) => ({ 26 | ...checkin, 27 | title: `Check-In #${idx + 1}`, 28 | dateFormatted: formatRelative(parseISO(checkin.created_at), new Date()), 29 | })); 30 | 31 | setCheckins(checkinList.reverse()); 32 | } 33 | 34 | async function getCheckins() { 35 | await api 36 | .get(`/students/${userID}/checkins`) 37 | .then(response => { 38 | configureData(response.data); 39 | }) 40 | .catch(error => { 41 | const message = error.response 42 | ? { 43 | title: 'Could not get checkins!', 44 | subtitle: error.response.data.error, 45 | } 46 | : { 47 | title: 'Network error!', 48 | subtitle: 'Check your connection.', 49 | }; 50 | 51 | Alert.alert(message.title, message.subtitle); 52 | }); 53 | } 54 | 55 | useEffect(() => { 56 | getCheckins(); 57 | }, []); 58 | 59 | async function handleCheckin() { 60 | await api 61 | .post(`/students/${userID}/checkins`) 62 | .then(async response => { 63 | await getCheckins(response.data); 64 | }) 65 | .catch(error => { 66 | const message = error.response 67 | ? { 68 | title: 'Check-in error!', 69 | subtitle: error.response.data.error, 70 | } 71 | : { 72 | title: 'Network error!', 73 | subtitle: 'Check your connection.', 74 | }; 75 | 76 | Alert.alert(message.title, message.subtitle); 77 | }); 78 | } 79 | 80 | return ( 81 | 82 | 83 | checkin.id.toString()} 86 | ListEmptyComponent={ 87 | 88 | You don't have any checkin yet. 89 | 90 | } 91 | renderItem={({ item }) => ( 92 | 93 | {item.title} 94 | {item.dateFormatted} 95 | 96 | )} 97 | /> 98 | 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /mobile/ios/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' 3 | 4 | target 'mobile' do 5 | # Pods for mobile 6 | pod 'FBLazyVector', :path => "../node_modules/react-native/Libraries/FBLazyVector" 7 | pod 'FBReactNativeSpec', :path => "../node_modules/react-native/Libraries/FBReactNativeSpec" 8 | pod 'RCTRequired', :path => "../node_modules/react-native/Libraries/RCTRequired" 9 | pod 'RCTTypeSafety', :path => "../node_modules/react-native/Libraries/TypeSafety" 10 | pod 'React', :path => '../node_modules/react-native/' 11 | pod 'React-Core', :path => '../node_modules/react-native/' 12 | pod 'React-CoreModules', :path => '../node_modules/react-native/React/CoreModules' 13 | pod 'React-Core/DevSupport', :path => '../node_modules/react-native/' 14 | pod 'React-RCTActionSheet', :path => '../node_modules/react-native/Libraries/ActionSheetIOS' 15 | pod 'React-RCTAnimation', :path => '../node_modules/react-native/Libraries/NativeAnimation' 16 | pod 'React-RCTBlob', :path => '../node_modules/react-native/Libraries/Blob' 17 | pod 'React-RCTImage', :path => '../node_modules/react-native/Libraries/Image' 18 | pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS' 19 | pod 'React-RCTNetwork', :path => '../node_modules/react-native/Libraries/Network' 20 | pod 'React-RCTSettings', :path => '../node_modules/react-native/Libraries/Settings' 21 | pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text' 22 | pod 'React-RCTVibration', :path => '../node_modules/react-native/Libraries/Vibration' 23 | pod 'React-Core/RCTWebSocket', :path => '../node_modules/react-native/' 24 | 25 | pod 'React-cxxreact', :path => '../node_modules/react-native/ReactCommon/cxxreact' 26 | pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi' 27 | pod 'React-jsiexecutor', :path => '../node_modules/react-native/ReactCommon/jsiexecutor' 28 | pod 'React-jsinspector', :path => '../node_modules/react-native/ReactCommon/jsinspector' 29 | pod 'ReactCommon/jscallinvoker', :path => "../node_modules/react-native/ReactCommon" 30 | pod 'ReactCommon/turbomodule/core', :path => "../node_modules/react-native/ReactCommon" 31 | pod 'Yoga', :path => '../node_modules/react-native/ReactCommon/yoga' 32 | 33 | pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec' 34 | pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec' 35 | pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec' 36 | 37 | pod 'RNGestureHandler', :path => '../node_modules/react-native-gesture-handler' 38 | 39 | pod 'RNSVG', :path => '../node_modules/react-native-svg' 40 | 41 | target 'mobileTests' do 42 | inherit! :search_paths 43 | # Pods for testing 44 | end 45 | 46 | use_native_modules! 47 | end 48 | 49 | target 'mobile-tvOS' do 50 | # Pods for mobile-tvOS 51 | 52 | target 'mobile-tvOSTests' do 53 | inherit! :search_paths 54 | # Pods for testing 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /frontend/src/pages/Enrollments/FormEnrollment/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { darken } from 'polished'; 3 | 4 | export const Container = styled.div` 5 | max-width: 1200px; 6 | margin: 50px auto; 7 | display: flex; 8 | flex-direction: column; 9 | 10 | form { 11 | display: flex; 12 | flex-direction: column; 13 | background-color: #fff; 14 | padding: 40px 30px; 15 | border-radius: 5px; 16 | 17 | div { 18 | margin: 0px 0 10px; 19 | } 20 | 21 | select, 22 | input { 23 | width: 100%; 24 | background: #ffff; 25 | border: 1px solid #cccc; 26 | border-radius: 4px; 27 | height: 44px; 28 | padding: 0 15px; 29 | color: #222; 30 | font-size: 16px; 31 | margin-top: 10px; 32 | 33 | &::placeholder { 34 | color: #9999; 35 | } 36 | 37 | &:disabled { 38 | background: #ddd; 39 | 40 | &:hover { 41 | cursor: not-allowed; 42 | } 43 | } 44 | } 45 | 46 | span { 47 | color: #ee4d64; 48 | align-self: center; 49 | font-weight: bold; 50 | margin-top: 15px; 51 | } 52 | 53 | strong { 54 | text-transform: uppercase; 55 | } 56 | 57 | button { 58 | margin: 5px 0 0; 59 | height: 44px; 60 | background: #ee4d64; 61 | font-weight: bold; 62 | color: #fff; 63 | border: 0; 64 | border-radius: 4px; 65 | font-size: 16px; 66 | transition: background 0.2s; 67 | 68 | &:hover { 69 | background: ${darken(0.03, '#ee4d64')}; 70 | } 71 | } 72 | 73 | a { 74 | color: #fff; 75 | margin-top: 15px; 76 | font-size: 16px; 77 | opacity: 0.8; 78 | 79 | &:hover { 80 | opacity: 1; 81 | } 82 | } 83 | 84 | .react-select__value-container, 85 | .react-select__control { 86 | height: 42px; 87 | } 88 | 89 | .react-select__value-container, 90 | .react-select__indicator { 91 | margin-bottom: 50px; 92 | } 93 | 94 | .react-select__placeholder { 95 | display: none; 96 | } 97 | 98 | .react-select__single-value { 99 | background: #ee4d64; 100 | color: #fff; 101 | padding: 6px 15px; 102 | border-radius: 2px; 103 | } 104 | } 105 | `; 106 | 107 | export const HorizontalInputs = styled.div` 108 | display: flex; 109 | width: 100%; 110 | justify-content: space-between; 111 | 112 | div { 113 | width: 250px; 114 | margin-bottom: 10px; 115 | } 116 | `; 117 | 118 | export const TitleBar = styled.div` 119 | display: flex; 120 | justify-content: space-between; 121 | margin: 20px 0px; 122 | 123 | button { 124 | width: 80px; 125 | height: 30px; 126 | background: #ee4d64; 127 | font-weight: bold; 128 | color: #fff; 129 | border: 0; 130 | border-radius: 4px; 131 | font-size: 16px; 132 | transition: background 0.2s; 133 | 134 | &:hover { 135 | background: ${darken(0.03, '#EE4D64')}; 136 | } 137 | } 138 | `; 139 | -------------------------------------------------------------------------------- /frontend/src/pages/Plans/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Link, useHistory } from 'react-router-dom'; 3 | import { FaPlus, FaEdit, FaTrash } from 'react-icons/fa'; 4 | import { toast } from 'react-toastify'; 5 | 6 | import { Container, TitleBar, PlanTable } from './styles'; 7 | import api from '~/services/api'; 8 | import throwError from '~/services/error'; 9 | 10 | export default function Plans() { 11 | const [plans, setPlans] = useState([]); 12 | 13 | const history = useHistory(); 14 | 15 | async function loadPlans() { 16 | await api 17 | .get('plans') 18 | .then(response => { 19 | setPlans(response.data); 20 | }) 21 | .catch(error => { 22 | throwError(error); 23 | }); 24 | } 25 | 26 | useEffect(() => { 27 | loadPlans(); 28 | }, []); 29 | 30 | async function handleDelete(id) { 31 | await api 32 | .delete(`plans/${id}`) 33 | .then(() => { 34 | toast.success(`Success! Plan deleted.`, { 35 | autoClose: 5000, 36 | }); 37 | }) 38 | .catch(error => { 39 | throwError(error); 40 | }); 41 | 42 | loadPlans(); 43 | } 44 | 45 | function handleEdit(planID) { 46 | history.push(`/plans/${planID}`); 47 | } 48 | 49 | return ( 50 | 51 | 52 |

Plan Management

53 | 60 |
61 | 62 | 63 | 64 | 65 | TITLE 66 | DURATION 67 | PRICE /mo 68 | 69 | 70 | 71 | 72 | {plans.length === 0 ? ( 73 |
No plan registered.
74 | ) : ( 75 | plans.map(plan => ( 76 | 77 | {plan.title} 78 | 79 | {plan.duration} {plan.duration !== 1 ? 'months' : 'month'} 80 | 81 | U$ {plan.price} 82 | 83 |
84 | 87 | 97 |
98 | 99 | 100 | )) 101 | )} 102 | 103 |
104 |
105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /backend/src/app/controllers/HelpOrderController.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | 3 | import HelpOrder from '../models/HelpOrder'; 4 | import Student from '../models/Student'; 5 | import Queue from '../../lib/Queue'; 6 | import HelpOrderAnsweredMail from '../jobs/HelpOrderAnsweredMail'; 7 | 8 | class HelpOrderController { 9 | async index(req, res) { 10 | const condition = req.userID 11 | ? { answer: null } 12 | : { student_id: req.params.studentId }; 13 | 14 | const orders = await HelpOrder.findAll({ 15 | where: condition, 16 | order: [['created_at', 'DESC']], 17 | attributes: ['id', 'created_at', 'question', 'answer', 'answer_at'], 18 | include: [ 19 | { 20 | model: Student, 21 | as: 'student', 22 | attributes: ['id', 'name'], 23 | }, 24 | ], 25 | }); 26 | 27 | return res.json(orders); 28 | } 29 | 30 | async store(req, res) { 31 | const schema = Yup.object().shape({ 32 | question: Yup.string().required(), 33 | }); 34 | 35 | if (!(await schema.isValid(req.body))) { 36 | return res.status(400).json({ 37 | error: 'Validation fails', 38 | }); 39 | } 40 | 41 | const { studentId: student_id } = req.params; 42 | const { question } = req.body; 43 | 44 | const studentExist = await Student.findByPk(student_id); 45 | 46 | if (!studentExist) { 47 | return res.status(400).json({ error: 'Student not found' }); 48 | } 49 | 50 | const { id } = await HelpOrder.create({ 51 | question, 52 | student_id, 53 | }); 54 | 55 | return res.json({ 56 | id, 57 | question, 58 | }); 59 | } 60 | 61 | async update(req, res) { 62 | const schema = Yup.object().shape({ 63 | answer: Yup.string().required(), 64 | }); 65 | 66 | if (!(await schema.isValid(req.body))) { 67 | return res.status(400).json({ 68 | error: 'Validation fails', 69 | }); 70 | } 71 | 72 | const { orderId } = req.params; 73 | const { answer } = req.body; 74 | 75 | const orderExist = await HelpOrder.findByPk(orderId); 76 | 77 | if (!orderExist) { 78 | return res.status(400).json({ error: 'Help order not found' }); 79 | } 80 | 81 | const answer_at = new Date(); 82 | 83 | await orderExist.update({ 84 | answer, 85 | answer_at, 86 | }); 87 | 88 | const { id, question, createdAt, student } = await HelpOrder.findByPk( 89 | orderId, 90 | { 91 | include: [ 92 | { 93 | model: Student, 94 | as: 'student', 95 | attributes: ['id', 'name', 'email'], 96 | }, 97 | ], 98 | } 99 | ); 100 | 101 | await Queue.add(HelpOrderAnsweredMail.key, { 102 | student, 103 | question, 104 | answer, 105 | }); 106 | 107 | return res.json({ 108 | id, 109 | question, 110 | answer, 111 | answer_at, 112 | createdAt, 113 | student, 114 | }); 115 | } 116 | } 117 | 118 | export default new HelpOrderController(); 119 | -------------------------------------------------------------------------------- /mobile/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem http://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /mobile/ios/mobile/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /mobile/src/pages/HelpOrders/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { TouchableOpacity } from 'react-native-gesture-handler'; 3 | import { Alert } from 'react-native'; 4 | import { useSelector } from 'react-redux'; 5 | 6 | import Icon from 'react-native-vector-icons/FontAwesome'; 7 | 8 | import { parseISO, formatRelative } from 'date-fns'; 9 | 10 | import { 11 | Container, 12 | OrderList, 13 | OrderItem, 14 | Status, 15 | OccurredAt, 16 | Question, 17 | Header, 18 | Empty, 19 | TextEmpty, 20 | StatusArea, 21 | } from './styles'; 22 | 23 | import Button from '~/components/Button'; 24 | import api from '~/services/api'; 25 | 26 | export default function HelpOrders(props) { 27 | const userID = useSelector(state => state.auth.id); 28 | const [orders, setOrders] = useState([]); 29 | const [isFetching, setIsFetching] = useState(false); 30 | 31 | async function configureData(data) { 32 | const orderList = await data.map(order => ({ 33 | ...order, 34 | dateFormatted: formatRelative(parseISO(order.created_at), new Date()), 35 | answered: !!order.answer_at, 36 | })); 37 | 38 | setOrders(orderList); 39 | setIsFetching(false); 40 | } 41 | 42 | async function getOrders() { 43 | await api 44 | .get(`students/${userID}/help-orders`) 45 | .then(response => { 46 | configureData(response.data); 47 | }) 48 | .catch(error => { 49 | const message = error.response 50 | ? { 51 | title: 'Could not get your help orders!', 52 | subtitle: error.response.data.error, 53 | } 54 | : { 55 | title: 'Network error!', 56 | subtitle: 'Check your connection.', 57 | }; 58 | 59 | Alert.alert(message.title, message.subtitle); 60 | }); 61 | } 62 | 63 | useEffect(() => { 64 | getOrders(); 65 | }, []); 66 | 67 | function handleOrder(id) { 68 | const { navigation } = props; 69 | 70 | const order = orders.find(o => o.id === id); 71 | 72 | navigation.navigate('Order', { order }); 73 | } 74 | 75 | function handleNewOrder() { 76 | const { navigation } = props; 77 | navigation.navigate('NewOrder', { refreshItems: getOrders }); 78 | } 79 | 80 | function onRefresh() { 81 | setIsFetching(true); 82 | getOrders(); 83 | } 84 | 85 | return ( 86 | 87 | 88 | 89 | 93 | You don't have any help orders yet. 94 | 95 | } 96 | keyExtractor={order => order.id.toString()} 97 | onRefresh={() => onRefresh()} 98 | refreshing={isFetching} 99 | renderItem={({ item }) => ( 100 | handleOrder(item.id)} 103 | > 104 | 105 |
106 | {item.answered ? ( 107 | 108 | 109 | answered 110 | 111 | ) : ( 112 | 113 | no reply 114 | 115 | )} 116 | 117 | {item.dateFormatted} 118 |
119 | {item.question} 120 |
121 |
122 | )} 123 | /> 124 |
125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /frontend/src/pages/HelpOrders/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { MdQuestionAnswer } from 'react-icons/md'; 3 | import ReactModal from 'react-modal'; 4 | import { toast } from 'react-toastify'; 5 | 6 | import { Container, TitleBar, OrdersTable, ContentModal } from './styles'; 7 | import api from '~/services/api'; 8 | import throwError from '~/services/error'; 9 | 10 | export default function HelpOrders() { 11 | const [showModal, setShowModal] = useState(false); 12 | const [orders, setOrders] = useState([]); 13 | const [selectedOrder, setSelectedOrder] = useState({}); 14 | const [answer, setAnswer] = useState(''); 15 | const [notAnswer, setNotAnswer] = useState(false); 16 | 17 | async function loadOrders() { 18 | await api 19 | .get('help-orders') 20 | .then(response => { 21 | setOrders(response.data); 22 | }) 23 | .catch(error => { 24 | throwError(error); 25 | }); 26 | } 27 | 28 | useEffect(() => { 29 | loadOrders(); 30 | }, []); 31 | 32 | async function handleClick(id) { 33 | const item = await orders.find(o => o.id === id); 34 | 35 | await setSelectedOrder(item); 36 | 37 | setShowModal(true); 38 | } 39 | 40 | async function handleSubmit() { 41 | if (answer !== '') { 42 | setNotAnswer(false); 43 | await api 44 | .put(`help-orders/${selectedOrder.id}`, { 45 | answer, 46 | }) 47 | .then(() => { 48 | toast.success('Success! The answer has been sent!'); 49 | loadOrders(); 50 | }) 51 | .catch(error => { 52 | throwError(error); 53 | }); 54 | } else { 55 | return setNotAnswer(true); 56 | } 57 | return setShowModal(false); 58 | } 59 | 60 | function handleClose() { 61 | setShowModal(false); 62 | } 63 | 64 | return ( 65 | <> 66 | 67 | 68 |

Help Orders

69 |
70 | 71 | 72 | 73 | 74 | STUDENT 75 | 76 | 77 | 78 | 79 | {orders.length === 0 ? ( 80 |
No help order registered.
81 | ) : ( 82 | orders.map(order => ( 83 | 84 | {order.student.name} 85 | 86 | 89 | 90 | 91 | )) 92 | )} 93 | 94 |
95 | handleClose()} 111 | shouldCloseOnOverlayClick 112 | > 113 | 114 |
115 | STUDENT QUESTION 116 |

{selectedOrder.question}

117 |
118 |
119 | YOUR ANSWER 120 | {notAnswer && ( 121 | Please, write an answer to this question. 122 | )} 123 |