├── 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 |
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 |
28 |
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 |
5 |
6 |
7 |
8 |
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 |
3 |
4 |
5 |
6 |
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 |
5 |
6 |
7 |
8 |
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 |
128 |
129 |
130 |
131 | >
132 | );
133 | }
134 |
--------------------------------------------------------------------------------
/frontend/src/pages/Enrollments/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Link, useHistory } from 'react-router-dom';
3 | import { format } from 'date-fns-tz';
4 | import { FaPlus, FaEdit, FaTrash, FaCircle } from 'react-icons/fa';
5 |
6 | import { parseISO } from 'date-fns';
7 | import { toast } from 'react-toastify';
8 | import { Container, TitleBar, EnrollmentTable } from './styles';
9 | import api from '~/services/api';
10 | import throwError from '~/services/error';
11 |
12 | export default function Enrollments() {
13 | const [enrollments, setEnrollments] = useState([]);
14 | const history = useHistory();
15 |
16 | async function configureEnrollments(data) {
17 | const dataEnrollments = await data.map(enrollment => ({
18 | ...enrollment,
19 | startDateFormatted: format(parseISO(enrollment.start_date), 'PP'),
20 | endDateFormatted: format(parseISO(enrollment.end_date), 'PP'),
21 | }));
22 |
23 | setEnrollments(dataEnrollments);
24 | }
25 |
26 | async function loadEnrollments() {
27 | await api
28 | .get('enrollment/')
29 | .then(response => {
30 | configureEnrollments(response.data);
31 | })
32 | .catch(error => {
33 | throwError(error);
34 | });
35 | }
36 |
37 | useEffect(() => {
38 | loadEnrollments();
39 | }, []);
40 |
41 | async function handleDelete(id) {
42 | await api
43 | .delete(`enrollment/${id}`)
44 | .then(() => {
45 | toast.success(`Success! Enrollment deleted.`, {
46 | autoClose: 5000,
47 | });
48 | })
49 | .catch(error => {
50 | throwError(error);
51 | });
52 |
53 | loadEnrollments();
54 | }
55 |
56 | function handleEdit(studentID) {
57 | history.push(`/enrollments/${studentID}`);
58 | }
59 |
60 | return (
61 |
62 |
63 | Enrollment Management
64 |
65 |
66 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | | STUDENT |
76 | PLAN |
77 | START |
78 | END |
79 | ACTIVE |
80 |
81 | |
82 |
83 |
84 |
85 | {enrollments.length === 0 ? (
86 | No enrollment registered.
87 | ) : (
88 | enrollments.map(enrollment => (
89 |
90 | |
91 | {enrollment.student
92 | ? enrollment.student.name
93 | : 'Deleted student'}
94 | |
95 |
96 | {enrollment.plan ? enrollment.plan.title : 'Deleted plan'}
97 | |
98 | {enrollment.startDateFormatted} |
99 | {enrollment.endDateFormatted} |
100 |
101 | {enrollment.active ? (
102 |
103 | ) : (
104 |
105 | )}
106 | |
107 |
108 |
109 |
115 |
125 |
126 | |
127 |
128 | ))
129 | )}
130 |
131 |
132 |
133 | );
134 | }
135 |
--------------------------------------------------------------------------------
/frontend/src/pages/Plans/FormPlan/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useMemo } from 'react';
2 | import * as Yup from 'yup';
3 | import { Form, Input } from '@rocketseat/unform';
4 | import { Link, useHistory } from 'react-router-dom';
5 | import { toast } from 'react-toastify';
6 | import { Container, TitleBar, HorizontalInputs } from './styles';
7 | import api from '~/services/api';
8 | import throwError from '~/services/error';
9 |
10 | const schema = Yup.object().shape({
11 | title: Yup.string().required('Title is required'),
12 | duration: Yup.number()
13 | .typeError('Insert a valid number')
14 | .required('Duration is required')
15 | .positive('This value should be a positive'),
16 | price: Yup.number()
17 | .typeError('Insert a valid number')
18 | .required('Price is required')
19 | .positive('This value should be a positive'),
20 | });
21 |
22 | export default function FormPrice({ match }) {
23 | const [mode, setMode] = useState('New');
24 | const [plan, setPlan] = useState([]);
25 | const [price, setPrice] = useState((0).toFixed(2));
26 | const [duration, setDuration] = useState(0);
27 | const history = useHistory();
28 | const { planID } = match.params;
29 |
30 | const totalPrice = useMemo(() => {
31 | return (price * duration).toFixed(2);
32 | }, [price, duration]);
33 |
34 | useEffect(() => {
35 | async function loadPlan() {
36 | const { data } = await api.get('plans');
37 |
38 | const item = await data.filter(object => object.id === Number(planID))[0];
39 |
40 | setPlan(item);
41 | setPrice(item.price);
42 | setDuration(item.duration);
43 | }
44 |
45 | if (planID) {
46 | loadPlan();
47 | setMode('Edit');
48 | }
49 | }, [match]);
50 |
51 | async function create(data) {
52 | const { title } = data;
53 |
54 | await api
55 | .post('plans', {
56 | title,
57 | duration,
58 | price,
59 | })
60 | .then(() => {
61 | toast.success(`Success! Plan created.`, {
62 | autoClose: 5000,
63 | });
64 |
65 | history.push('/plans');
66 | })
67 | .catch(error => {
68 | throwError(error);
69 | });
70 | }
71 |
72 | async function edit(data) {
73 | const { title } = data;
74 |
75 | await api
76 | .put(`plans/${planID}`, {
77 | title,
78 | duration,
79 | price,
80 | })
81 | .then(() => {
82 | toast.success(`Success! Plan updated.`, {
83 | autoClose: 5000,
84 | });
85 | history.push('/plans');
86 | })
87 | .catch(error => {
88 | throwError(error);
89 | });
90 | }
91 |
92 | async function handleSubmit(data) {
93 | mode === 'New' ? create(data) : edit(data);
94 | }
95 |
96 | return (
97 |
98 |
99 | {mode === 'New' ? Plan Registration
: Plan name
}
100 |
101 |
102 |
103 |
104 |
105 |
106 |
149 |
150 | );
151 | }
152 |
--------------------------------------------------------------------------------
/frontend/src/pages/Students/FormStudent/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import * as Yup from 'yup';
3 | import { Form, Input } from '@rocketseat/unform';
4 | import { Link, useHistory } from 'react-router-dom';
5 | import { toast } from 'react-toastify';
6 | import { Container, TitleBar, HorizontalInputs } from './styles';
7 | import api from '~/services/api';
8 | import throwError from '~/services/error';
9 |
10 | const schema = Yup.object().shape({
11 | name: Yup.string().required('Name is required'),
12 | email: Yup.string()
13 | .email('Insert a valid e-mail')
14 | .required('E-mail is required'),
15 | weight: Yup.number()
16 | .typeError('Insert a valid number')
17 | .required('Age is required')
18 | .positive('This number must be positive'),
19 | height: Yup.number()
20 | .typeError('Insert a valid number')
21 | .required('Age is required')
22 | .positive('This number must be positive'),
23 | age: Yup.number()
24 | .typeError('Insert a valid number')
25 | .required('Age is required')
26 | .positive('This number must be positive')
27 | .integer(),
28 | });
29 |
30 | export default function FormStudent({ match }) {
31 | const [mode, setMode] = useState('New');
32 | const [student, setStudent] = useState([]);
33 | const history = useHistory();
34 | const { studentID } = match.params;
35 |
36 | useEffect(() => {
37 | async function loadStudent() {
38 | const { data } = await api.get('students');
39 |
40 | const item = await data.filter(object => object.id === Number(studentID));
41 |
42 | await setStudent(item[0]);
43 | }
44 |
45 | if (studentID) {
46 | loadStudent();
47 | setMode('Edit');
48 | }
49 | }, [match]);
50 |
51 | async function create(data) {
52 | const { name, email, age, height, weight } = data;
53 |
54 | await api
55 | .post('students', {
56 | name,
57 | email,
58 | age,
59 | height,
60 | weight,
61 | })
62 | .then(() => {
63 | toast.success(`Success! Student created.`, {
64 | autoClose: 5000,
65 | });
66 | history.push('/students');
67 | })
68 | .catch(error => {
69 | throwError(error);
70 | });
71 | }
72 |
73 | async function edit(data) {
74 | const { name, email, age, height, weight } = data;
75 |
76 | await api
77 | .put(`students/${studentID}`, {
78 | name,
79 | email,
80 | age,
81 | height,
82 | weight,
83 | })
84 | .then(() => {
85 | toast.success(`Success! Student edited.`, {
86 | autoClose: 5000,
87 | });
88 | history.push('/students');
89 | })
90 | .catch(error => {
91 | throwError(error);
92 | });
93 | }
94 |
95 | async function handleSubmit(data) {
96 | mode === 'New' ? create(data) : edit(data);
97 | }
98 |
99 | return (
100 |
101 |
102 | {mode === 'New' ? Student Registration
: Student Name
}
103 |
104 |
105 |
106 |
107 |
108 |
109 |
151 |
152 | );
153 | }
154 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | > Gym management using Javascript stacks: node.js (backend), ReactJS (frontend) and React Native (mobile) for Android app.
11 |
12 | > This project is being built for the final challenge of Rocketseat's GoStack bootcamp.
13 |
14 | ## Requirements
15 | - [x] Back-end: [Challenge 02](https://github.com/Rocketseat/bootcamp-gostack-desafio-02)
16 | - [x] Back-end: [Challenge 03](https://github.com/Rocketseat/bootcamp-gostack-desafio-03)
17 | - [x] Front-end web: [Challenge 09](https://github.com/Rocketseat/bootcamp-gostack-desafio-09)
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 | ### 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 | ### Front-end web
44 |
45 | - [x] [React](http://reactjs.org)
46 | - [x] [Redux](https://redux.js.org/)
47 | - [x] [Redux Saga](https://github.com/redux-saga/redux-saga)
48 | - [x] [React Router DOM](https://www.npmjs.com/package/react-router-dom)
49 | - [x] [Styled Components](https://www.styled-components.com/)
50 | - [x] [Redux Persist](https://github.com/rt2zz/redux-persist)
51 | - [x] [Reactotron](https://github.com/infinitered/reactotron)
52 | - [x] [Axios](https://github.com/axios/axios)
53 | - [x] [date-fns](https://date-fns.org/)
54 | - [x] [Polished](https://github.com/styled-components/polished)
55 | - [x] [React-Toastify](https://github.com/fkhadra/react-toastify)
56 | - [x] [@Rocketseat/Unform](https://github.com/Rocketseat/unform)
57 | - [x] [ESLint](https://eslint.org/)
58 | - [x] [Prettier](https://prettier.io/)
59 |
60 | ### Mobile
61 |
62 | - [x] [React Native](https://facebook.github.io/react-native/)
63 | - [x] [React Navigation](https://reactnavigation.org/)
64 | - [x] [Redux](https://redux.js.org/)
65 | - [x] [Redux Saga](https://github.com/redux-saga/redux-saga)
66 | - [x] [Styled Components](https://www.styled-components.com/)
67 | - [x] [Redux Persist](https://github.com/rt2zz/redux-persist)
68 | - [x] [Reactotron](https://github.com/infinitered/reactotron)
69 | - [x] [Axios](https://github.com/axios/axios)
70 | - [x] [ESLint](https://eslint.org/)
71 | - [x] [Prettier](https://prettier.io/)
72 |
73 | ## Installation
74 |
75 | ```sh
76 | # create and start two images on docker: database (postgres) and redis.
77 | docker run --name database -e POSTGRES_PASSWORD=password -p 5432:5432 -d postgres
78 | docker run --name redisbase -p 6379:6379 -d -t redis:alpine
79 | docker start database redisbase
80 | ```
81 | ### Backend
82 | ```sh
83 | # install all dependencies
84 | yarn install
85 |
86 | # execute sequelize migrations and seeds
87 | yarn sequelize db:migrate
88 | yarn sequelize db:seed:all
89 |
90 | # start the e-mail queue service
91 | yarn queue
92 |
93 | # start the app on a new terminal window
94 | yarn dev
95 | ```
96 |
97 | ### Frontend web
98 | ```sh
99 | # install all dependencies
100 | yarn install
101 |
102 | # start the app on a new terminal window
103 | yarn start
104 | ```
105 |
106 | ### Mobile
107 | ```sh
108 | # install all dependencies
109 | yarn install
110 |
111 | # start the app on a new terminal window
112 | # this app was developed only for android.
113 | react-native run-android
114 | react-native start
115 | ```
116 |
117 | ## Author
118 |
119 | 👤 **Nathan Ribeiro**
120 |
121 | * Linkedin: [Nathan Ribeiro](https://www.linkedin.com/in/nathanfribeiro/)
122 | * Github: [@NathanFRibeiro](https://github.com/NathanFRibeiro)
123 |
124 | ## Show your support
125 |
126 | Give a ⭐️ if you like this project!
127 |
128 | ***
129 | _Made with ❤️ by [Nathan Ribeiro](https://github.com/NathanFRibeiro)_
130 |
--------------------------------------------------------------------------------
/backend/src/app/controllers/EnrollmentController.js:
--------------------------------------------------------------------------------
1 | import * as Yup from 'yup';
2 | import { addMonths, parseISO, addHours } from 'date-fns';
3 |
4 | import Enrollment from '../models/Enrollment';
5 | import Student from '../models/Student';
6 | import Plan from '../models/Plan';
7 | import Queue from '../../lib/Queue';
8 | import NewEnrollmentMail from '../jobs/NewEnrollmentMail';
9 |
10 | class EnrollmentController {
11 | async index(req, res) {
12 | const { id } = req.params;
13 |
14 | const condition = id ? { canceled_at: null, id } : { canceled_at: null };
15 |
16 | const enrollments = await Enrollment.findAll({
17 | where: condition,
18 | attributes: ['id', 'start_date', 'end_date', 'price', 'active'],
19 | include: [
20 | {
21 | model: Student,
22 | as: 'student',
23 | attributes: ['id', 'name', 'age'],
24 | },
25 | {
26 | model: Plan,
27 | as: 'plan',
28 | attributes: ['id', 'title', 'duration', 'price'],
29 | },
30 | ],
31 | });
32 |
33 | return res.json(enrollments);
34 | }
35 |
36 | async store(req, res) {
37 | const schema = Yup.object().shape({
38 | student_id: Yup.number().required(),
39 | plan_id: Yup.number().required(),
40 | start_date: Yup.date().required(),
41 | });
42 |
43 | if (!(await schema.isValid(req.body))) {
44 | return res.status(400).json({ error: 'Validation fails' });
45 | }
46 |
47 | const { student_id, plan_id } = req.body;
48 |
49 | const studentExist = await Student.findByPk(student_id);
50 |
51 | if (!studentExist) {
52 | return res.status(400).json({ error: 'Student not found' });
53 | }
54 |
55 | const planExist = await Plan.findByPk(plan_id);
56 |
57 | if (!planExist) {
58 | return res.status(400).json({ error: 'Plan not found' });
59 | }
60 |
61 | const start_date = addHours(parseISO(req.body.start_date), 1);
62 | const end_date = addMonths(start_date, planExist.duration);
63 | const price = planExist.price * planExist.duration;
64 |
65 | await Enrollment.create({
66 | start_date,
67 | plan_id,
68 | student_id,
69 | end_date,
70 | price,
71 | });
72 |
73 | await Queue.add(NewEnrollmentMail.key, {
74 | studentExist,
75 | planExist,
76 | start_date,
77 | end_date,
78 | price,
79 | });
80 |
81 | return res.json({
82 | start_date,
83 | end_date,
84 | price,
85 | plan: planExist,
86 | student: studentExist,
87 | });
88 | }
89 |
90 | async update(req, res) {
91 | const schema = Yup.object().shape({
92 | plan_id: Yup.number(),
93 | start_date: Yup.date(),
94 | });
95 |
96 | if (!(await schema.isValid(req.body))) {
97 | return res.status(400).json({ error: 'Validation fails' });
98 | }
99 |
100 | const { id } = req.params;
101 | const { plan_id } = req.body;
102 |
103 | const enrollment = await Enrollment.findByPk(id);
104 |
105 | const start_date =
106 | req.body.start_date && req.body.start_date !== enrollment.start_date
107 | ? parseISO(req.body.start_date)
108 | : enrollment.start_date;
109 |
110 | let { end_date, price } = enrollment;
111 |
112 | if (plan_id && plan_id !== enrollment.plan_id) {
113 | const planExist = await Plan.findByPk(plan_id);
114 | if (!planExist) {
115 | return res.status(400).json({ error: 'Plan not found' });
116 | }
117 |
118 | end_date = addMonths(start_date, planExist.duration);
119 | price = planExist.price * planExist.duration;
120 | }
121 |
122 | await enrollment.update({
123 | ...req.body,
124 | end_date,
125 | price,
126 | });
127 |
128 | const { plan, student } = await Enrollment.findByPk(id, {
129 | include: [
130 | {
131 | model: Plan,
132 | as: 'plan',
133 | attributes: ['id', 'title', 'duration', 'price'],
134 | },
135 | {
136 | model: Student,
137 | as: 'student',
138 | attributes: ['id', 'name', 'age', 'height', 'weight'],
139 | },
140 | ],
141 | });
142 |
143 | return res.json({
144 | id,
145 | start_date,
146 | end_date,
147 | price,
148 | plan,
149 | student,
150 | });
151 | }
152 |
153 | async delete(req, res) {
154 | const enrollment = await Enrollment.findByPk(req.params.id);
155 |
156 | enrollment.canceled_at = new Date();
157 |
158 | await enrollment.save();
159 |
160 | return res.json(enrollment);
161 | }
162 | }
163 |
164 | export default new EnrollmentController();
165 |
--------------------------------------------------------------------------------