├── frontend
├── README.md
├── src
│ ├── store
│ │ ├── modules
│ │ │ ├── user
│ │ │ │ ├── actions.js
│ │ │ │ ├── sagas.js
│ │ │ │ └── reducer.js
│ │ │ ├── rootReducer.js
│ │ │ ├── rootSaga.js
│ │ │ └── auth
│ │ │ │ ├── actions.js
│ │ │ │ ├── reducer.js
│ │ │ │ └── sagas.js
│ │ ├── persistReducers.js
│ │ ├── createStore.js
│ │ └── index.js
│ ├── assets
│ │ ├── logo-header.png
│ │ ├── search.svg
│ │ └── logo-header.svg
│ ├── services
│ │ ├── history.js
│ │ └── api.js
│ ├── index.js
│ ├── pages
│ │ ├── _layouts
│ │ │ ├── default
│ │ │ │ ├── styles.js
│ │ │ │ └── index.js
│ │ │ └── auth
│ │ │ │ ├── index.js
│ │ │ │ └── styles.js
│ │ ├── SignIn
│ │ │ └── index.js
│ │ ├── HelpOrders
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── Plans
│ │ │ ├── ManagePlans
│ │ │ │ └── styles.js
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── Students
│ │ │ ├── ManageStudent
│ │ │ │ └── styles.js
│ │ │ └── styles.js
│ │ └── Memberships
│ │ │ ├── ManageMembership
│ │ │ └── styles.js
│ │ │ └── styles.js
│ ├── components
│ │ ├── Loading
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── Header
│ │ │ ├── Menu
│ │ │ │ ├── index.js
│ │ │ │ └── styles.js
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── Pagination
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── HeightInput
│ │ │ └── index.js
│ │ └── Modal
│ │ │ ├── index.js
│ │ │ └── styles.js
│ ├── styles
│ │ ├── colors.js
│ │ └── global.js
│ ├── helpers
│ │ ├── format.js
│ │ ├── LinkWrapper
│ │ │ └── index.js
│ │ └── hooks
│ │ │ └── useComponentVisible.js
│ ├── config
│ │ └── ReactotronConfig.js
│ ├── App.js
│ └── routes
│ │ ├── Route.js
│ │ └── index.js
├── .prettierrc
├── public
│ ├── favicon.ico
│ └── index.html
├── jsconfig.json
├── .editorconfig
├── config-overrides.js
├── .gitignore
├── .eslintrc.js
└── package.json
├── mobile
├── .watchmanconfig
├── .gitattributes
├── app.json
├── .prettierrc
├── src
│ ├── assets
│ │ ├── logo.png
│ │ ├── 8-layers.png
│ │ └── logo-header.png
│ ├── components
│ │ ├── Background
│ │ │ ├── index.js
│ │ │ └── BackgroundSignIn
│ │ │ │ └── index.js
│ │ ├── Input
│ │ │ ├── index.js
│ │ │ └── styles.js
│ │ ├── Button
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── Header
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ └── Order
│ │ │ ├── styles.js
│ │ │ └── index.js
│ ├── store
│ │ ├── modules
│ │ │ ├── rootReducer.js
│ │ │ ├── rootSaga.js
│ │ │ └── student
│ │ │ │ ├── actions.js
│ │ │ │ ├── reducer.js
│ │ │ │ └── sagas.js
│ │ ├── createStore.js
│ │ ├── persistReducers.js
│ │ └── index.js
│ ├── services
│ │ └── api.js
│ ├── index.js
│ ├── pages
│ │ ├── HelpOrdersPages
│ │ │ ├── HelpOrders
│ │ │ │ └── styles.js
│ │ │ ├── NewOrder
│ │ │ │ ├── styles.js
│ │ │ │ └── index.js
│ │ │ └── Answer
│ │ │ │ ├── styles.js
│ │ │ │ └── index.js
│ │ ├── SignIn
│ │ │ ├── styles.js
│ │ │ └── index.js
│ │ ├── CheckInsPage
│ │ │ └── Checkins
│ │ │ │ └── styles.js
│ │ └── ProfilePage
│ │ │ └── Profile
│ │ │ ├── styles.js
│ │ │ └── index.js
│ ├── config
│ │ └── ReactotronConfig.js
│ ├── App.js
│ └── routes.js
├── android
│ ├── app
│ │ ├── debug.keystore
│ │ ├── src
│ │ │ ├── main
│ │ │ │ ├── res
│ │ │ │ │ ├── values
│ │ │ │ │ │ ├── strings.xml
│ │ │ │ │ │ └── styles.xml
│ │ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ │ └── mipmap-xxxhdpi
│ │ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── java
│ │ │ │ │ └── com
│ │ │ │ │ │ └── mobile
│ │ │ │ │ │ ├── MainActivity.java
│ │ │ │ │ │ └── MainApplication.java
│ │ │ │ └── AndroidManifest.xml
│ │ │ └── debug
│ │ │ │ └── AndroidManifest.xml
│ │ ├── proguard-rules.pro
│ │ ├── build_defs.bzl
│ │ └── BUCK
│ ├── gradle
│ │ └── wrapper
│ │ │ ├── gradle-wrapper.jar
│ │ │ └── gradle-wrapper.properties
│ ├── settings.gradle
│ ├── gradle.properties
│ ├── build.gradle
│ └── gradlew.bat
├── ios
│ ├── mobile
│ │ ├── Images.xcassets
│ │ │ ├── Contents.json
│ │ │ └── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ ├── AppDelegate.h
│ │ ├── main.m
│ │ ├── AppDelegate.m
│ │ ├── Info.plist
│ │ └── Base.lproj
│ │ │ └── LaunchScreen.xib
│ ├── mobileTests
│ │ ├── Info.plist
│ │ └── mobileTests.m
│ ├── mobile-tvOSTests
│ │ └── Info.plist
│ ├── mobile-tvOS
│ │ └── Info.plist
│ └── Podfile
├── jsconfig.json
├── .buckconfig
├── .editorconfig
├── babel.config.js
├── index.js
├── __tests__
│ └── App-test.js
├── metro.config.js
├── .gitignore
├── .eslintrc.js
├── package.json
└── .flowconfig
├── backend
├── src
│ ├── server.js
│ ├── queue.js
│ ├── assets
│ │ └── logo.png
│ ├── bootstrap.js
│ ├── config
│ │ ├── auth.js
│ │ ├── redis.js
│ │ ├── mail.js
│ │ └── database.js
│ ├── app
│ │ ├── views
│ │ │ └── emails
│ │ │ │ ├── partials
│ │ │ │ ├── header.hbs
│ │ │ │ └── footer.hbs
│ │ │ │ ├── layouts
│ │ │ │ └── default.hbs
│ │ │ │ ├── help-order-answer.hbs
│ │ │ │ └── confirmation.hbs
│ │ ├── models
│ │ │ ├── Plan.js
│ │ │ ├── Checkin.js
│ │ │ ├── Student.js
│ │ │ ├── Order.js
│ │ │ ├── User.js
│ │ │ └── Membership.js
│ │ ├── jobs
│ │ │ ├── HelpOrderAnswerMail.js
│ │ │ └── ConfirmationMail.js
│ │ ├── middlewares
│ │ │ └── auth.js
│ │ └── controllers
│ │ │ ├── SessionController.js
│ │ │ ├── AnswerController.js
│ │ │ ├── OrderController.js
│ │ │ ├── UserController.js
│ │ │ ├── PlanController.js
│ │ │ ├── CheckinController.js
│ │ │ └── StudentController.js
│ ├── database
│ │ ├── seeds
│ │ │ └── 20191016115833-admin-user.js
│ │ ├── index.js
│ │ └── migrations
│ │ │ ├── 20191019155243-create-checkins.js
│ │ │ ├── 20191016121858-create-users.js
│ │ │ ├── 20191018151913-plan-management.js
│ │ │ ├── 20191019173908-create-help-orders.js
│ │ │ ├── 20191016133943-create-students.js
│ │ │ └── 20191018173726-membership-management.js
│ ├── lib
│ │ ├── Queue.js
│ │ └── Mail.js
│ ├── app.js
│ └── routes.js
├── .prettierrc
├── nodemon.json
├── .editorconfig
├── .env.test
├── .env.example
├── .sequelizerc
├── __tests__
│ ├── util
│ │ └── truncate.js
│ ├── getToken.js
│ ├── factories.js
│ └── integration
│ │ ├── session.test.js
│ │ └── answer.test.js
├── .vscode
│ └── launch.json
├── .gitignore
├── .eslintrc.js
└── package.json
├── renovate.json
└── LICENSE
/frontend/README.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mobile/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/frontend/src/store/modules/user/actions.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/user/sagas.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mobile/.gitattributes:
--------------------------------------------------------------------------------
1 | *.pbxproj -text
2 |
--------------------------------------------------------------------------------
/backend/src/server.js:
--------------------------------------------------------------------------------
1 | import app from './app';
2 |
3 | app.listen(3003);
4 |
--------------------------------------------------------------------------------
/mobile/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mobile",
3 | "displayName": "mobile"
4 | }
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/backend/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5"
4 | }
--------------------------------------------------------------------------------
/frontend/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5"
4 | }
--------------------------------------------------------------------------------
/backend/src/queue.js:
--------------------------------------------------------------------------------
1 | import Queue from './lib/Queue';
2 |
3 | Queue.processQueue();
4 |
--------------------------------------------------------------------------------
/mobile/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5",
4 | }
5 |
--------------------------------------------------------------------------------
/backend/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/backend/src/assets/logo.png
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/mobile/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/src/assets/logo.png
--------------------------------------------------------------------------------
/mobile/src/assets/8-layers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/src/assets/8-layers.png
--------------------------------------------------------------------------------
/mobile/android/app/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/debug.keystore
--------------------------------------------------------------------------------
/mobile/src/assets/logo-header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/src/assets/logo-header.png
--------------------------------------------------------------------------------
/backend/src/bootstrap.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config({
2 | path: process.env.NODE_ENV === 'test' ? '.env.test' : '.env',
3 | });
4 |
--------------------------------------------------------------------------------
/frontend/src/assets/logo-header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/frontend/src/assets/logo-header.png
--------------------------------------------------------------------------------
/mobile/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | mobile
3 |
4 |
--------------------------------------------------------------------------------
/mobile/ios/mobile/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/backend/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "execMap": {
3 | "js": "node -r sucrase/register"
4 | },
5 | "ignore": [
6 | "__tests__"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/backend/src/config/auth.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 |
3 | export default {
4 | secret: process.env.APP_SECRET,
5 | expiresIn: '7d',
6 | };
7 |
--------------------------------------------------------------------------------
/frontend/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "paths": {
5 | "~/*": ["*"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/mobile/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "paths": {
5 | "~/*": ["*"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/mobile/.buckconfig:
--------------------------------------------------------------------------------
1 |
2 | [android]
3 | target = Google Inc.:Google APIs:23
4 |
5 | [maven_repositories]
6 | central = https://repo1.maven.org/maven2
7 |
--------------------------------------------------------------------------------
/mobile/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/backend/src/config/redis.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 |
3 | export default {
4 | host: process.env.REDIS_HOST,
5 | port: process.env.REDIS_PORT,
6 | };
7 |
--------------------------------------------------------------------------------
/frontend/src/services/history.js:
--------------------------------------------------------------------------------
1 | import { createBrowserHistory } from 'history';
2 |
3 | const history = createBrowserHistory();
4 |
5 | export default history;
6 |
--------------------------------------------------------------------------------
/frontend/src/services/api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const api = axios.create({
4 | baseURL: 'http://localhost:3003',
5 | });
6 |
7 | export default api;
8 |
--------------------------------------------------------------------------------
/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/mobile/src/components/Background/index.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components/native';
2 |
3 | export default styled.View`
4 | flex: 1;
5 | background: #f5f5f5;
6 | `;
7 |
--------------------------------------------------------------------------------
/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | ReactDOM.render(, document.getElementById('root'));
6 |
--------------------------------------------------------------------------------
/frontend/src/pages/_layouts/default/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Wrapper = styled.div`
4 | height: 100%;
5 | background: #f5f5f5;
6 | `;
7 |
--------------------------------------------------------------------------------
/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/mobile/src/components/Background/BackgroundSignIn/index.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components/native';
2 |
3 | export default styled.View`
4 | flex: 1;
5 | background: #fff;
6 | `;
7 |
--------------------------------------------------------------------------------
/mobile/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | indent_style = space
6 | indent_size = 2
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
--------------------------------------------------------------------------------
/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/StefanoSaffran/gympoint/HEAD/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/frontend/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 | end_of_line = lf
--------------------------------------------------------------------------------
/mobile/src/store/modules/rootReducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import student from './student/reducer';
4 |
5 | export default combineReducers({
6 | student,
7 | });
8 |
--------------------------------------------------------------------------------
/mobile/src/store/modules/rootSaga.js:
--------------------------------------------------------------------------------
1 | import { all } from 'redux-saga/effects';
2 |
3 | import student from './student/sagas';
4 |
5 | export default function* rootSaga() {
6 | return yield all([student]);
7 | }
8 |
--------------------------------------------------------------------------------
/mobile/android/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'mobile'
2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
3 | include ':app'
4 |
--------------------------------------------------------------------------------
/backend/src/app/views/emails/partials/header.hbs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/rootReducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import auth from './auth/reducer';
4 | import user from './user/reducer';
5 |
6 | export default combineReducers({
7 | auth,
8 | user,
9 | });
10 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/rootSaga.js:
--------------------------------------------------------------------------------
1 | import { all } from 'redux-saga/effects';
2 |
3 | import auth from './auth/sagas';
4 | import user from './user/sagas';
5 |
6 | export default function* rootSaga() {
7 | return yield all([auth, user]);
8 | }
9 |
--------------------------------------------------------------------------------
/backend/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 | end_of_line = lf
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/frontend/src/components/Loading/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Container = styled.div`
4 | height: 100%;
5 | padding-top: 72px;
6 |
7 | @media (min-width: 768px) {
8 | padding-left: 50%;
9 | }
10 | `;
11 |
--------------------------------------------------------------------------------
/mobile/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:metro-react-native-babel-preset'],
3 | plugins: [
4 | [
5 | 'babel-plugin-root-import',
6 | {
7 | rootPathSuffix: 'src',
8 | },
9 | ],
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/backend/src/app/views/emails/partials/footer.hbs:
--------------------------------------------------------------------------------
1 |
⌚️ Horario de funcionamento:
2 |
3 | Segunda a sexta: 06:00 - 22:00
4 | Sabados e domingos: 08:00 - 18:00
5 |
6 |
7 | Equipe GymPoint 💪💪
8 |
--------------------------------------------------------------------------------
/frontend/config-overrides.js:
--------------------------------------------------------------------------------
1 | const { addBabelPlugin, override } = require('customize-cra');
2 |
3 | module.exports = override(
4 | addBabelPlugin([
5 | 'babel-plugin-root-import',
6 | {
7 | rootPathSuffix: 'src',
8 | },
9 | ])
10 | );
11 |
--------------------------------------------------------------------------------
/mobile/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/frontend/src/styles/colors.js:
--------------------------------------------------------------------------------
1 | export default {
2 | primary: '#ee4d64',
3 | editButton: '#4d85ee',
4 | backButton: '#ccc',
5 | white: '#fff',
6 | border: '#ddd',
7 | lightBorder: '#eee',
8 | lightGray: '#999',
9 | gray: '#666',
10 | darkGray: '#444',
11 | };
12 |
--------------------------------------------------------------------------------
/mobile/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @format
3 | */
4 |
5 | import { AppRegistry } from 'react-native';
6 | import 'react-native-gesture-handler';
7 | import App from './src';
8 | import { name as appName } from './app.json';
9 |
10 | AppRegistry.registerComponent(appName, () => App);
11 |
--------------------------------------------------------------------------------
/frontend/src/helpers/format.js:
--------------------------------------------------------------------------------
1 | export const unFormat = target =>
2 | target
3 | .toString()
4 | .replace(/[R$.]/g, '')
5 | .replace(',', '.');
6 |
7 | export const { format: formatPrice } = new Intl.NumberFormat('pt-BR', {
8 | style: 'currency',
9 | currency: 'BRL',
10 | });
11 |
--------------------------------------------------------------------------------
/backend/src/app/views/emails/layouts/default.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{> header }}
3 | {{{ body }}}
4 | {{> footer }}
5 |
6 |
--------------------------------------------------------------------------------
/backend/.env.test:
--------------------------------------------------------------------------------
1 | APP_URL=http://localhost:3003
2 | NODE_ENV=test
3 |
4 | # Auth
5 |
6 | APP_SECRET=secrettests
7 |
8 | # Database
9 |
10 | DB_DIALECT=sqlite
11 |
12 | # Redis
13 |
14 | REDIS_HOST=127.0.0.1
15 | REDIS_POST=6379
16 |
17 | # Mail
18 |
19 | MAIL_HOST=
20 | MAIL_PORT=
21 | MAIL_USER=
22 | MAIL_PASS=
23 |
--------------------------------------------------------------------------------
/frontend/src/helpers/LinkWrapper/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NavLink } from 'react-router-dom';
3 |
4 | import colors from '~/styles/colors';
5 |
6 | const LinkWrapper = props => {
7 | return ;
8 | };
9 | export default LinkWrapper;
10 |
--------------------------------------------------------------------------------
/mobile/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/mobile/__tests__/App-test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @format
3 | */
4 |
5 | import 'react-native';
6 | import React from 'react';
7 | import App from '../App';
8 |
9 | // Note: test renderer must be required after react-native.
10 | import renderer from 'react-test-renderer';
11 |
12 | it('renders correctly', () => {
13 | renderer.create();
14 | });
15 |
--------------------------------------------------------------------------------
/backend/.env.example:
--------------------------------------------------------------------------------
1 | APP_URL=http://localhost:3003
2 | NODE_ENV=development
3 |
4 | # Auth
5 |
6 | APP_SECRET=
7 |
8 | # Database
9 |
10 | DB_HOST=
11 | DB_USER=
12 | DB_PASS=
13 | DB_NAME=
14 |
15 | # Redis
16 |
17 | REDIS_HOST=127.0.0.1
18 | REDIS_POST=6379
19 |
20 | # Mail
21 |
22 | MAIL_HOST=
23 | MAIL_PORT=
24 | MAIL_USER=
25 | MAIL_PASS=
26 |
27 |
--------------------------------------------------------------------------------
/backend/.sequelizerc:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path');
2 |
3 | module.exports = {
4 | config: resolve(__dirname, 'src', 'config', 'database.js'),
5 | 'models-path': resolve(__dirname, 'src', 'app', 'models'),
6 | 'migrations-path': resolve(__dirname, 'src', 'database', 'migrations'),
7 | 'seeders-path': resolve(__dirname, 'src', 'database', 'seeds'),
8 | }
9 |
--------------------------------------------------------------------------------
/backend/src/config/mail.js:
--------------------------------------------------------------------------------
1 | require('dotenv/config');
2 |
3 | export default {
4 | host: process.env.MAIL_HOST,
5 | port: process.env.MAIL_PORT,
6 | secure: false,
7 | auth: {
8 | user: process.env.MAIL_USER,
9 | pass: process.env.MAIL_PASS,
10 | },
11 | default: {
12 | from: 'Equipe GymPoint ',
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/mobile/src/store/createStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 |
3 | export default (reducers, middlewares) => {
4 | const enhancer = __DEV__
5 | ? compose(console.tron.createEnhancer(), applyMiddleware(...middlewares))
6 | : applyMiddleware(...middlewares);
7 |
8 | return createStore(reducers, enhancer);
9 | };
10 |
--------------------------------------------------------------------------------
/backend/__tests__/util/truncate.js:
--------------------------------------------------------------------------------
1 | import database from '../../src/database';
2 |
3 | export default function truncate() {
4 | return Promise.all(
5 | Object.keys(database.connection.models).map(key => {
6 | return database.connection.models[key].destroy({
7 | truncate: true,
8 | force: true,
9 | });
10 | })
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/mobile/src/services/api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | /*
4 | url: {
5 | ios: localhost
6 | android: {
7 | android studio: 10.0.2.2,
8 | genymotion: 10.0.3.2,
9 | phone via usb: your ip
10 | }
11 | }
12 | */
13 |
14 | const api = axios.create({
15 | baseURL: 'http://10.0.2.2:3003',
16 | });
17 |
18 | export default api;
19 |
--------------------------------------------------------------------------------
/backend/src/app/views/emails/help-order-answer.hbs:
--------------------------------------------------------------------------------
1 | Olá, {{ student }}
2 |
3 | Obrigado por entrar em contado, confira os detalhes de seu pedido abaixo:
4 |
5 | Sua pergunta: {{ question }}
6 | Resposta do instrutor: {{ answer }}
7 | Se ainda restarem duvidas, favor criar um novo pedido.
8 |
9 |
--------------------------------------------------------------------------------
/mobile/metro.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Metro configuration for React Native
3 | * https://github.com/facebook/react-native
4 | *
5 | * @format
6 | */
7 |
8 | module.exports = {
9 | transformer: {
10 | getTransformOptions: async () => ({
11 | transform: {
12 | experimentalImportSupport: false,
13 | inlineRequires: false,
14 | },
15 | }),
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/mobile/src/components/Input/index.js:
--------------------------------------------------------------------------------
1 | import React, {forwardRef} from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import {Container, TInput} from './styles';
5 |
6 | const Input = ({style, ...rest}, ref) => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default forwardRef(Input);
15 |
--------------------------------------------------------------------------------
/frontend/src/store/persistReducers.js:
--------------------------------------------------------------------------------
1 | import storage from 'redux-persist/lib/storage';
2 | import { persistReducer } from 'redux-persist';
3 |
4 | export default reducers => {
5 | const persistedReducer = persistReducer(
6 | {
7 | key: 'gympoint',
8 | storage,
9 | whitelist: ['auth', 'user'],
10 | },
11 | reducers
12 | );
13 |
14 | return persistedReducer;
15 | };
16 |
--------------------------------------------------------------------------------
/frontend/src/pages/_layouts/auth/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { Wrapper, Content } from './styles';
5 |
6 | export default function AuthLayout({ children }) {
7 | return (
8 |
9 | {children}
10 |
11 | );
12 | }
13 |
14 | AuthLayout.propTypes = {
15 | children: PropTypes.element.isRequired,
16 | };
17 |
--------------------------------------------------------------------------------
/mobile/src/store/persistReducers.js:
--------------------------------------------------------------------------------
1 | import AsyncStorage from '@react-native-community/async-storage';
2 | import { persistReducer } from 'redux-persist';
3 |
4 | export default reducers => {
5 | const persistedReducer = persistReducer(
6 | {
7 | key: 'gympoint',
8 | storage: AsyncStorage,
9 | whitelist: ['student'],
10 | },
11 | reducers
12 | );
13 |
14 | return persistedReducer;
15 | };
16 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/mobile/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/user/reducer.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | const initial_state = {
4 | profile: null,
5 | };
6 |
7 | export default function user(state = initial_state, action) {
8 | switch (action.type) {
9 | case '@auth/SIGN_IN_SUCCESS':
10 | return produce(state, draft => {
11 | draft.profile = action.payload.user;
12 | });
13 | default:
14 | return state;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/backend/src/app/models/Plan.js:
--------------------------------------------------------------------------------
1 | import Sequelize, { Model } from 'sequelize';
2 |
3 | class Plan extends Model {
4 | static init(sequelize) {
5 | super.init(
6 | {
7 | title: Sequelize.STRING,
8 | duration: Sequelize.INTEGER,
9 | price: Sequelize.DOUBLE,
10 | },
11 | {
12 | sequelize,
13 | }
14 | );
15 |
16 | return this;
17 | }
18 | }
19 |
20 | export default Plan;
21 |
--------------------------------------------------------------------------------
/backend/src/app/models/Checkin.js:
--------------------------------------------------------------------------------
1 | import { Model } from 'sequelize';
2 |
3 | class Checkin extends Model {
4 | static init(sequelize) {
5 | super.init(
6 | {},
7 | {
8 | sequelize,
9 | }
10 | );
11 |
12 | return this;
13 | }
14 |
15 | static associate(models) {
16 | this.belongsTo(models.Student, { foreignKey: 'student_id', as: 'student' });
17 | }
18 | }
19 |
20 | export default Checkin;
21 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Challenge 9
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/frontend/src/config/ReactotronConfig.js:
--------------------------------------------------------------------------------
1 | import Reactotron from 'reactotron-react-js';
2 | import { reactotronRedux } from 'reactotron-redux';
3 | import reactotronSaga from 'reactotron-redux-saga';
4 |
5 | if (process.env.NODE_ENV === 'development') {
6 | const tron = Reactotron.configure()
7 | .use(reactotronRedux())
8 | .use(reactotronSaga())
9 | .connect();
10 |
11 | tron.clear();
12 |
13 | console.tron = tron;
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/store/createStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 |
3 | export default (reducers, middlewares) => {
4 | const enhancer =
5 | process.env.NODE_ENV === 'development'
6 | ? compose(
7 | console.tron.createEnhancer(),
8 | applyMiddleware(...middlewares)
9 | )
10 | : applyMiddleware(...middlewares);
11 |
12 | return createStore(reducers, enhancer);
13 | };
14 |
--------------------------------------------------------------------------------
/mobile/src/components/Button/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components/native';
2 | import { RectButton } from 'react-native-gesture-handler';
3 |
4 | export const Container = styled(RectButton)`
5 | height: 45px;
6 | background: #ee4e62;
7 | border-radius: 4px;
8 | align-items: center;
9 | justify-content: center;
10 | `;
11 |
12 | export const Text = styled.Text`
13 | color: #fff;
14 | font-weight: bold 700;
15 | font-size: 16px;
16 | `;
17 |
--------------------------------------------------------------------------------
/backend/src/config/database.js:
--------------------------------------------------------------------------------
1 | require('../bootstrap');
2 |
3 | module.exports = {
4 | dialect: process.env.DB_DIALECT || 'postgres',
5 | host: process.env.DB_HOST,
6 | username: process.env.DB_USER,
7 | password: process.env.DB_PASS,
8 | database: process.env.DB_NAME,
9 | storage: './__tests__/database.sqlite',
10 | logging: false,
11 | define: {
12 | timestamps: true,
13 | underscored: true,
14 | underscoredAll: true,
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/backend/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "attach",
10 | "name": "Launch Program",
11 | "restart": true,
12 | "protocol": "inspector"
13 | }
14 | ]
15 | }
--------------------------------------------------------------------------------
/backend/__tests__/getToken.js:
--------------------------------------------------------------------------------
1 | import request from 'supertest';
2 |
3 | import app from '../src/app';
4 | import factory from './factories';
5 |
6 | const getToken = async () => {
7 | const user = await factory.attrs('User');
8 |
9 | await request(app)
10 | .post('/users')
11 | .send(user);
12 |
13 | const response = await request(app)
14 | .post('/sessions')
15 | .send(user);
16 |
17 | return response;
18 | };
19 |
20 | export default getToken;
21 |
--------------------------------------------------------------------------------
/frontend/src/pages/_layouts/default/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import Header from '~/components/Header';
5 |
6 | import { Wrapper } from './styles';
7 |
8 | export default function DefaultLayout({ children }) {
9 | return (
10 |
11 |
12 | {children}
13 |
14 | );
15 | }
16 |
17 | DefaultLayout.propTypes = {
18 | children: PropTypes.element.isRequired,
19 | };
20 |
--------------------------------------------------------------------------------
/mobile/ios/mobile/AppDelegate.h:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Facebook, Inc. and its affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | #import
9 | #import
10 |
11 | @interface AppDelegate : UIResponder
12 |
13 | @property (nonatomic, strong) UIWindow *window;
14 |
15 | @end
16 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /__tests__/coverage/*
10 | /__tests__/database.sqlite
11 |
12 | # production
13 | /dist
14 |
15 | # misc
16 | .DS_Store
17 | .env
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
--------------------------------------------------------------------------------
/mobile/ios/mobile/main.m:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Facebook, Inc. and its affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | #import
9 |
10 | #import "AppDelegate.h"
11 |
12 | int main(int argc, char * argv[]) {
13 | @autoreleasepool {
14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/mobile/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
--------------------------------------------------------------------------------
/mobile/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { PersistGate } from 'redux-persist/integration/react';
3 | import { Provider } from 'react-redux';
4 |
5 | import './config/ReactotronConfig';
6 |
7 | import { store, persistor } from './store';
8 | import App from './App';
9 |
10 | export default function Index() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/mobile/src/components/Input/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components/native';
2 |
3 | export const Container = styled.View`
4 | padding: 0 10px;
5 | height: 45px;
6 | background: #fff;
7 | border-radius: 4px;
8 | flex-direction: row;
9 | align-items: center;
10 | border: 1px solid #ddd;
11 | `;
12 |
13 | export const TInput = styled.TextInput.attrs({
14 | placeholderTextColor: '#a3a3a3',
15 | })`
16 | flex: 1;
17 | font-size: 16px;
18 | margin-left: 10px;
19 | color: #333;
20 | `;
21 |
--------------------------------------------------------------------------------
/mobile/src/pages/HelpOrdersPages/HelpOrders/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components/native';
2 |
3 | import Button from '~/components/Button';
4 |
5 | export const Container = styled.SafeAreaView`
6 | flex: 1;
7 | padding: 0 30px;
8 | `;
9 |
10 | export const NewOrderButton = styled(Button)`
11 | margin: 20px 0 10px;
12 | `;
13 |
14 | export const HelpOrderList = styled.FlatList.attrs({
15 | showsVerticalScrollIndicator: false,
16 | contentContainerStyle: { paddingTop: 10, paddingBottom: 10 },
17 | })``;
18 |
--------------------------------------------------------------------------------
/backend/src/database/seeds/20191016115833-admin-user.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcryptjs');
2 |
3 | module.exports = {
4 | up: queryInterface => {
5 | return queryInterface.bulkInsert(
6 | 'users',
7 | [
8 | {
9 | name: 'Administrador',
10 | email: 'admin@gympoint.com',
11 | password_hash: bcrypt.hashSync('123456', 8),
12 | created_at: new Date(),
13 | updated_at: new Date(),
14 | },
15 | ],
16 | {}
17 | );
18 | },
19 |
20 | down: () => {},
21 | };
22 |
--------------------------------------------------------------------------------
/mobile/src/config/ReactotronConfig.js:
--------------------------------------------------------------------------------
1 | import Reactotron from 'reactotron-react-native';
2 | import AsyncStorage from '@react-native-community/async-storage';
3 | import { reactotronRedux } from 'reactotron-redux';
4 | import reactotronSaga from 'reactotron-redux-saga';
5 |
6 | if (__DEV__) {
7 | const tron = Reactotron.setAsyncStorageHandler(AsyncStorage)
8 | .configure()
9 | .useReactNative()
10 | .use(reactotronRedux())
11 | .use(reactotronSaga())
12 | .connect();
13 |
14 | console.tron = tron;
15 |
16 | tron.clear();
17 | }
18 |
--------------------------------------------------------------------------------
/backend/src/app/models/Student.js:
--------------------------------------------------------------------------------
1 | import Sequelize, { Model } from 'sequelize';
2 | // import { differenceInYears, parseISO } from 'date-fns';
3 |
4 | class Student extends Model {
5 | static init(sequelize) {
6 | super.init(
7 | {
8 | name: Sequelize.STRING,
9 | email: Sequelize.STRING,
10 | age: Sequelize.INTEGER,
11 | weight: Sequelize.FLOAT,
12 | height: Sequelize.FLOAT,
13 | },
14 | {
15 | sequelize,
16 | }
17 | );
18 | return this;
19 | }
20 | }
21 |
22 | export default Student;
23 |
--------------------------------------------------------------------------------
/backend/src/app/models/Order.js:
--------------------------------------------------------------------------------
1 | import Sequelize, { Model } from 'sequelize';
2 |
3 | class Order extends Model {
4 | static init(sequelize) {
5 | super.init(
6 | {
7 | question: Sequelize.STRING,
8 | answer: Sequelize.STRING,
9 | answer_at: Sequelize.DATE,
10 | },
11 | {
12 | sequelize,
13 | }
14 | );
15 |
16 | return this;
17 | }
18 |
19 | static associate(models) {
20 | this.belongsTo(models.Student, { foreignKey: 'student_id', as: 'student' });
21 | }
22 | }
23 |
24 | export default Order;
25 |
--------------------------------------------------------------------------------
/mobile/src/store/modules/student/actions.js:
--------------------------------------------------------------------------------
1 | export function signInRequest(id) {
2 | return {
3 | type: '@student/SIGN_IN_REQUEST',
4 | payload: { id },
5 | };
6 | }
7 |
8 | export function signInSuccess(membership, student) {
9 | return {
10 | type: '@student/SIGN_IN_SUCCESS',
11 | payload: { membership, student },
12 | };
13 | }
14 |
15 | export function signFailure() {
16 | return {
17 | type: '@student/SIGN_FAILURE',
18 | };
19 | }
20 |
21 | export function signOut() {
22 | return {
23 | type: '@student/SIGN_OUT',
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/backend/src/app/views/emails/confirmation.hbs:
--------------------------------------------------------------------------------
1 | Olá, {{ student }}
2 |
3 | Sua matricula foi realizada com sucesso, seja bem vindo(a) a GymPoint. 💪
4 | Confira os detalhes abaixo:
5 |
6 | 📅 Data da Matrícula: {{ start_date }}
7 | 🚀 Plano: {{ plan }}
8 | 💵 Preço Total: {{ price }}
9 | 📆 Vigência: {{ duration }}
10 | 🆔 ID: {{ id }}
11 | Use seu ID para acessar seu perfil no app mobile.
12 |
13 |
--------------------------------------------------------------------------------
/mobile/src/pages/HelpOrdersPages/NewOrder/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components/native';
2 |
3 | import Button from '~/components/Button';
4 | import Input from '~/components/Input';
5 |
6 | export const Container = styled.SafeAreaView`
7 | flex: 1;
8 | padding: 0 30px;
9 | margin-top: 20px;
10 | `;
11 |
12 | export const OrderTextArea = styled(Input).attrs({
13 | textAlignVertical: 'top',
14 | padding: 0,
15 | })`
16 | min-height: 180px;
17 | font-size: 16px;
18 | font-weight: 400;
19 | padding-top: 20px;
20 | line-height: 24px;
21 | `;
22 |
23 | export const SubmitButton = styled(Button)`
24 | margin: 20px 0 10px;
25 | `;
26 |
--------------------------------------------------------------------------------
/backend/src/app/jobs/HelpOrderAnswerMail.js:
--------------------------------------------------------------------------------
1 | import Mail from '../../lib/Mail';
2 |
3 | class HelpOrderAnswerMail {
4 | get key() {
5 | return 'HelpOrderAnswerMail';
6 | }
7 |
8 | async handle({ data }) {
9 | const { order } = data;
10 |
11 | await Mail.sendMail({
12 | to: `${order.student.name} <${order.student.email}>`,
13 | subject: 'Resposta para seu pedido na GymPoint',
14 | template: 'help-order-answer',
15 | context: {
16 | student: order.student.name,
17 | question: order.question,
18 | answer: order.answer,
19 | },
20 | });
21 | }
22 | }
23 |
24 | export default new HelpOrderAnswerMail();
25 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/auth/actions.js:
--------------------------------------------------------------------------------
1 | export function signInRequest(email, password) {
2 | return {
3 | type: '@auth/SIGN_IN_REQUEST',
4 | payload: { email, password },
5 | };
6 | }
7 |
8 | export function signInSuccess(token, user) {
9 | return {
10 | type: '@auth/SIGN_IN_SUCCESS',
11 | payload: { token, user },
12 | };
13 | }
14 |
15 | export function signFailure() {
16 | return {
17 | type: '@auth/SIGN_FAILURE',
18 | };
19 | }
20 |
21 | export function signOut() {
22 | return {
23 | type: '@auth/SIGN_OUT',
24 | };
25 | }
26 |
27 | export function tokenInvalid() {
28 | return {
29 | type: '@auth/TOKEN_INVALID',
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/mobile/src/components/Button/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ActivityIndicator } from 'react-native';
3 | import PropTypes from 'prop-types';
4 |
5 | import { Container, Text } from './styles';
6 |
7 | export default function Button({ children, loading, ...rest }) {
8 | return (
9 |
10 | {loading ? (
11 |
12 | ) : (
13 | {children}
14 | )}
15 |
16 | );
17 | }
18 |
19 | Button.propTypes = {
20 | children: PropTypes.string.isRequired,
21 | loading: PropTypes.bool,
22 | };
23 |
24 | Button.defaultProps = {
25 | loading: false,
26 | };
27 |
--------------------------------------------------------------------------------
/backend/src/app/middlewares/auth.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 | import { promisify } from 'util';
3 |
4 | import authConfig from '../../config/auth';
5 |
6 | export default async (req, res, next) => {
7 | const authHeader = req.headers.authorization;
8 |
9 | if (!authHeader) {
10 | return res.status(401).json({ error: 'Token not provided' });
11 | }
12 |
13 | const [, token] = authHeader.split(' ');
14 |
15 | try {
16 | const decodedToken = await promisify(jwt.verify)(token, authConfig.secret);
17 |
18 | req.userId = decodedToken.id;
19 |
20 | return next();
21 | } catch (err) {
22 | return res.status(401).json({ error: 'Token invalid' });
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/mobile/android/app/build_defs.bzl:
--------------------------------------------------------------------------------
1 | """Helper definitions to glob .aar and .jar targets"""
2 |
3 | def create_aar_targets(aarfiles):
4 | for aarfile in aarfiles:
5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")]
6 | lib_deps.append(":" + name)
7 | android_prebuilt_aar(
8 | name = name,
9 | aar = aarfile,
10 | )
11 |
12 | def create_jar_targets(jarfiles):
13 | for jarfile in jarfiles:
14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")]
15 | lib_deps.append(":" + name)
16 | prebuilt_jar(
17 | name = name,
18 | binary_jar = jarfile,
19 | )
20 |
--------------------------------------------------------------------------------
/frontend/src/components/Loading/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Spinner, Dots } from 'react-activity';
3 | import PropTypes from 'prop-types';
4 |
5 | import colors from '~/styles/colors';
6 | import { Container } from './styles';
7 |
8 | const Loading = ({ type }) => {
9 | if (type === 'spinner') {
10 | return (
11 |
12 |
13 |
14 | );
15 | }
16 | return ;
17 | };
18 |
19 | Loading.defaultProps = {
20 | type: '',
21 | };
22 |
23 | Loading.propTypes = {
24 | type: PropTypes.string,
25 | };
26 |
27 | export default Loading;
28 |
--------------------------------------------------------------------------------
/mobile/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { persistStore } from 'redux-persist';
2 | import createSagaMiddleware from 'redux-saga';
3 | import createStore from './createStore';
4 | import persistReducers from './persistReducers';
5 |
6 | import rootReducer from './modules/rootReducer';
7 | import rootSaga from './modules/rootSaga';
8 |
9 | const sagaMonitor = __DEV__ ? console.tron.createSagaMonitor() : null;
10 |
11 | const sagaMiddleware = createSagaMiddleware({ sagaMonitor });
12 |
13 | const middleware = [sagaMiddleware];
14 |
15 | const store = createStore(persistReducers(rootReducer), middleware);
16 | const persistor = persistStore(store);
17 |
18 | sagaMiddleware.run(rootSaga);
19 |
20 | export { store, persistor };
21 |
--------------------------------------------------------------------------------
/mobile/src/pages/SignIn/styles.js:
--------------------------------------------------------------------------------
1 | import { Platform } from 'react-native';
2 | import styled from 'styled-components/native';
3 |
4 | import Input from '~/components/Input';
5 | import Button from '~/components/Button';
6 |
7 | export const Container = styled.KeyboardAvoidingView.attrs({
8 | enabled: Platform.OS === 'ios',
9 | behavior: 'padding',
10 | })`
11 | flex: 1;
12 | justify-content: center;
13 | align-items: center;
14 | padding: 0 30px;
15 | `;
16 |
17 | export const Form = styled.View`
18 | align-self: stretch;
19 | margin-top: 25px;
20 | `;
21 |
22 | export const FormInput = styled(Input)`
23 | margin-bottom: 10px;
24 | `;
25 |
26 | export const SubmitButton = styled(Button)`
27 | margin-top: 5px;
28 | `;
29 |
--------------------------------------------------------------------------------
/mobile/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { StatusBar, YellowBox } from 'react-native';
4 | import FlashMessage from 'react-native-flash-message';
5 |
6 | import createRouter from './routes';
7 |
8 | YellowBox.ignoreWarnings(['Unrecognized WebSocket']);
9 |
10 | export default function App() {
11 | const signed = useSelector(state => state.student.signed);
12 |
13 | const Routes = createRouter(signed);
14 |
15 | return (
16 | <>
17 |
18 |
19 |
20 | >
21 | );
22 | }
23 | StatusBar.setTranslucent(true);
24 | StatusBar.setBarStyle('dark-content');
25 |
--------------------------------------------------------------------------------
/frontend/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { persistStore } from 'redux-persist';
2 | import createSagaMiddleware from 'redux-saga';
3 | import createStore from './createStore';
4 | import persistReducers from './persistReducers';
5 |
6 | import rootReducer from './modules/rootReducer';
7 | import rootSaga from './modules/rootSaga';
8 |
9 | const sagaMonitor =
10 | process.env.NODE_ENV === 'development'
11 | ? console.tron.createSagaMonitor()
12 | : null;
13 |
14 | const sagaMiddleware = createSagaMiddleware({ sagaMonitor });
15 |
16 | const middleware = [sagaMiddleware];
17 |
18 | const store = createStore(persistReducers(rootReducer), middleware);
19 | const persistor = persistStore(store);
20 |
21 | sagaMiddleware.run(rootSaga);
22 |
23 | export { store, persistor };
24 |
--------------------------------------------------------------------------------
/backend/src/app/models/User.js:
--------------------------------------------------------------------------------
1 | import Sequelize, { Model } from 'sequelize';
2 | import bcrypt from 'bcryptjs';
3 |
4 | class User extends Model {
5 | static init(sequelize) {
6 | super.init(
7 | {
8 | name: Sequelize.STRING,
9 | email: Sequelize.STRING,
10 | password: Sequelize.VIRTUAL,
11 | password_hash: Sequelize.STRING,
12 | },
13 | {
14 | sequelize,
15 | }
16 | );
17 |
18 | this.addHook('beforeSave', async user => {
19 | if (user.password) {
20 | user.password_hash = await bcrypt.hash(user.password, 8);
21 | }
22 | });
23 |
24 | return this;
25 | }
26 |
27 | checkPassword(password) {
28 | return bcrypt.compare(password, this.password_hash);
29 | }
30 | }
31 |
32 | export default User;
33 |
--------------------------------------------------------------------------------
/mobile/ios/mobile/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
--------------------------------------------------------------------------------
/mobile/src/components/Header/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components/native';
2 |
3 | import logo from '~/assets/logo-header.png';
4 |
5 | export const Container = styled.View`
6 | align-items: center;
7 | justify-content: center;
8 | padding-top: 15px;
9 | border: 1px solid #ddd;
10 | `;
11 |
12 | export const ContainerWithNav = styled.View`
13 | align-items: center;
14 | justify-content: center;
15 | padding-top: 15px;
16 | flex-direction: row;
17 | border: 1px solid #ddd;
18 | `;
19 |
20 | export const LogoWrapper = styled.View`
21 | flex: 1;
22 | margin-right: 50px;
23 | `;
24 |
25 | export const Logo = styled.Image.attrs({
26 | source: logo,
27 | })`
28 | margin: 20px 0;
29 | align-self: center;
30 | `;
31 |
32 | export const BackButton = styled.TouchableOpacity`
33 | margin: 0 0 0 20px;
34 | `;
35 |
--------------------------------------------------------------------------------
/backend/src/database/index.js:
--------------------------------------------------------------------------------
1 | import Sequelize from 'sequelize';
2 |
3 | import User from '../app/models/User';
4 | import Student from '../app/models/Student';
5 | import Plan from '../app/models/Plan';
6 | import Membership from '../app/models/Membership';
7 | import Checkin from '../app/models/Checkin';
8 | import Order from '../app/models/Order';
9 |
10 | import databaseConfig from '../config/database';
11 |
12 | const models = [User, Student, Plan, Membership, Checkin, Order];
13 |
14 | class Database {
15 | constructor() {
16 | this.init();
17 | }
18 |
19 | init() {
20 | this.connection = new Sequelize(databaseConfig);
21 |
22 | models
23 | .map(model => model.init(this.connection))
24 | .map(model => model.associate && model.associate(this.connection.models));
25 | }
26 | }
27 |
28 | export default new Database();
29 |
--------------------------------------------------------------------------------
/mobile/ios/mobileTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/frontend/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ToastContainer } from 'react-toastify';
3 | import { PersistGate } from 'redux-persist/integration/react';
4 | import { Router } from 'react-router-dom';
5 | import { Provider } from 'react-redux';
6 |
7 | import './config/ReactotronConfig';
8 |
9 | import Routes from './routes';
10 | import history from './services/history';
11 |
12 | import { store, persistor } from './store';
13 |
14 | import GlobalStyle from './styles/global';
15 |
16 | function App() {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
30 | export default App;
31 |
--------------------------------------------------------------------------------
/mobile/ios/mobile-tvOSTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/backend/src/database/migrations/20191019155243-create-checkins.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | up: (queryInterface, Sequelize) => {
3 | return queryInterface.createTable('checkins', {
4 | id: {
5 | type: Sequelize.INTEGER,
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | },
10 | student_id: {
11 | type: Sequelize.INTEGER,
12 | references: { model: 'students', key: 'id' },
13 | onUpdate: 'CASCADE',
14 | onDelete: 'CASCADE',
15 | allowNull: true,
16 | },
17 | created_at: {
18 | type: Sequelize.DATE,
19 | allowNull: false,
20 | },
21 | updated_at: {
22 | type: Sequelize.DATE,
23 | allowNull: false,
24 | },
25 | });
26 | },
27 |
28 | down: queryInterface => {
29 | return queryInterface.dropTable('checkins');
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/mobile/android/app/src/main/java/com/mobile/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.mobile;
2 |
3 | import com.facebook.react.ReactActivity;
4 | import com.facebook.react.ReactActivityDelegate;
5 | import com.facebook.react.ReactRootView;
6 | import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
7 |
8 | public class MainActivity extends ReactActivity {
9 |
10 | /**
11 | * Returns the name of the main component registered from JavaScript. This is used to schedule
12 | * rendering of the component.
13 | */
14 | @Override
15 | protected String getMainComponentName() {
16 | return "mobile";
17 | }
18 |
19 | @Override
20 | protected ReactActivityDelegate createReactActivityDelegate() {
21 | return new ReactActivityDelegate(this, getMainComponentName()) {
22 | @Override
23 | protected ReactRootView createRootView() {
24 | return new RNGestureHandlerEnabledRootView(MainActivity.this);
25 | }
26 | };
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/backend/src/database/migrations/20191016121858-create-users.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | up: (queryInterface, Sequelize) => {
3 | return queryInterface.createTable('users', {
4 | id: {
5 | type: Sequelize.INTEGER,
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | },
10 | name: {
11 | type: Sequelize.STRING,
12 | allowNull: false,
13 | },
14 | email: {
15 | type: Sequelize.STRING,
16 | allowNull: false,
17 | unique: true,
18 | },
19 | password_hash: {
20 | type: Sequelize.STRING,
21 | allowNull: false,
22 | },
23 | created_at: {
24 | type: Sequelize.DATE,
25 | allowNull: false,
26 | },
27 | updated_at: {
28 | type: Sequelize.DATE,
29 | allowNull: false,
30 | },
31 | });
32 | },
33 |
34 | down: queryInterface => {
35 | return queryInterface.dropTable('users');
36 | },
37 | };
38 |
--------------------------------------------------------------------------------
/backend/src/database/migrations/20191018151913-plan-management.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | up: (queryInterface, Sequelize) => {
3 | return queryInterface.createTable('plans', {
4 | id: {
5 | type: Sequelize.INTEGER,
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | },
10 | title: {
11 | type: Sequelize.STRING,
12 | allowNull: false,
13 | unique: true,
14 | },
15 | duration: {
16 | type: Sequelize.INTEGER,
17 | allowNull: false,
18 | },
19 | price: {
20 | type: Sequelize.DOUBLE,
21 | allowNull: false,
22 | },
23 | created_at: {
24 | type: Sequelize.DATE,
25 | allowNull: false,
26 | },
27 | updated_at: {
28 | type: Sequelize.DATE,
29 | allowNull: false,
30 | },
31 | });
32 | },
33 |
34 | down: queryInterface => {
35 | return queryInterface.dropTable('plans');
36 | },
37 | };
38 |
--------------------------------------------------------------------------------
/mobile/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | android.useAndroidX=true
21 | android.enableJetifier=true
22 |
--------------------------------------------------------------------------------
/backend/__tests__/factories.js:
--------------------------------------------------------------------------------
1 | import faker from 'faker';
2 | import { factory } from 'factory-girl';
3 |
4 | import User from '../src/app/models/User';
5 | import Student from '../src/app/models/Student';
6 | import Plan from '../src/app/models/Plan';
7 | import Checkin from '../src/app/models/Checkin';
8 |
9 | factory.define('User', User, () => ({
10 | name: faker.name.findName(),
11 | email: faker.internet.email(),
12 | password: faker.internet.password(),
13 | }));
14 |
15 | factory.define('Student', Student, () => ({
16 | name: faker.name.findName(),
17 | email: faker.internet.email(),
18 | age: faker.random.number(120),
19 | weight: faker.finance.amount(50, 250, 2),
20 | height: faker.finance.amount(1.2, 2.5, 2),
21 | }));
22 |
23 | factory.define('Plan', Plan, () => ({
24 | title: faker.random.word(),
25 | duration: faker.random.number({ min: 1, max: 12 }),
26 | price: faker.finance.amount(10, 200, 2),
27 | }));
28 |
29 | factory.define('Checkin', Checkin, {});
30 |
31 | export default factory;
32 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/auth/reducer.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | const initial_state = {
4 | token: null,
5 | signed: false,
6 | loading: false,
7 | };
8 |
9 | export default function auth(state = initial_state, action) {
10 | return produce(state, draft => {
11 | switch (action.type) {
12 | case '@auth/SIGN_IN_REQUEST': {
13 | draft.loading = true;
14 | break;
15 | }
16 | case '@auth/SIGN_IN_SUCCESS': {
17 | draft.token = action.payload.token;
18 | draft.signed = true;
19 | draft.loading = false;
20 | break;
21 | }
22 | case '@auth/SIGN_FAILURE': {
23 | draft.loading = false;
24 | break;
25 | }
26 | case '@auth/SIGN_OUT': {
27 | draft.token = null;
28 | draft.signed = false;
29 | break;
30 | }
31 | case '@auth/TOKEN_INVALID': {
32 | draft.token = null;
33 | draft.signed = false;
34 | break;
35 | }
36 | default:
37 | }
38 | });
39 | }
40 |
--------------------------------------------------------------------------------
/mobile/src/store/modules/student/reducer.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | const INITIAL_STATE = {
4 | profile: null,
5 | membership: null,
6 | signed: false,
7 | loading: false,
8 | };
9 |
10 | export default function student(state = INITIAL_STATE, action) {
11 | return produce(state, draft => {
12 | switch (action.type) {
13 | case '@student/SIGN_IN_REQUEST': {
14 | draft.loading = true;
15 | break;
16 | }
17 | case '@student/SIGN_IN_SUCCESS': {
18 | draft.profile = action.payload.student;
19 | draft.membership = action.payload.membership;
20 | draft.signed = true;
21 | draft.loading = false;
22 | break;
23 | }
24 | case '@student/SIGN_FAILURE': {
25 | draft.loading = false;
26 | break;
27 | }
28 | case '@student/SIGN_OUT': {
29 | draft.signed = false;
30 | draft.profile = null;
31 | draft.membership = null;
32 | break;
33 | }
34 | default:
35 | }
36 | });
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/src/helpers/hooks/useComponentVisible.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from 'react';
2 |
3 | export default function useComponentVisible(initialIsVisible) {
4 | const [isComponentVisible, setIsComponentVisible] = useState(
5 | initialIsVisible
6 | );
7 | const ref = useRef(null);
8 |
9 | const handleHideDropdown = event => {
10 | if (event.key === 'Escape') {
11 | setIsComponentVisible(false);
12 | }
13 | };
14 |
15 | const handleClickOutside = event => {
16 | if (ref.current && !ref.current.contains(event.target)) {
17 | setIsComponentVisible(false);
18 | }
19 | };
20 |
21 | useEffect(() => {
22 | document.addEventListener('keydown', handleHideDropdown, true);
23 | document.addEventListener('click', handleClickOutside, true);
24 | return () => {
25 | document.removeEventListener('keydown', handleHideDropdown, true);
26 | document.removeEventListener('click', handleClickOutside, true);
27 | };
28 | });
29 |
30 | return { ref, isComponentVisible, setIsComponentVisible };
31 | }
32 |
--------------------------------------------------------------------------------
/mobile/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 |
24 | # Android/IntelliJ
25 | #
26 | build/
27 | .idea
28 | .gradle
29 | local.properties
30 | *.iml
31 |
32 | # node.js
33 | #
34 | node_modules/
35 | npm-debug.log
36 | yarn-error.log
37 |
38 | # BUCK
39 | buck-out/
40 | \.buckd/
41 | *.keystore
42 | !debug.keystore
43 |
44 | # fastlane
45 | #
46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
47 | # screenshots whenever they are needed.
48 | # For more information about the recommended setup visit:
49 | # https://docs.fastlane.tools/best-practices/source-control/
50 |
51 | */fastlane/report.xml
52 | */fastlane/Preview.html
53 | */fastlane/screenshots
54 |
55 | # Bundle artifact
56 | *.jsbundle
57 |
58 | # CocoaPods
59 | /ios/Pods/
60 |
--------------------------------------------------------------------------------
/mobile/src/pages/CheckInsPage/Checkins/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components/native';
2 |
3 | import Button from '~/components/Button';
4 |
5 | export const Container = styled.SafeAreaView`
6 | flex: 1;
7 | padding: 0 30px;
8 | `;
9 |
10 | export const CheckinButton = styled(Button)`
11 | margin: 20px 0 10px;
12 | `;
13 |
14 | export const CheckinList = styled.FlatList.attrs({
15 | showsVerticalScrollIndicator: false,
16 | contentContainerStyle: { paddingTop: 10, paddingBottom: 10 },
17 | })``;
18 |
19 | export const Checkin = styled.View`
20 | flex-direction: row;
21 | align-items: center;
22 | justify-content: space-between;
23 | height: 46px;
24 | border-radius: 4px;
25 | border: 1px solid #ddd;
26 | background-color: #fff;
27 | padding: 0 20px;
28 | margin: 5px 0;
29 | `;
30 |
31 | export const Left = styled.Text`
32 | height: 17px;
33 | color: #444;
34 | font-size: 14px;
35 | font-weight: 700;
36 | `;
37 |
38 | export const Right = styled.Text`
39 | height: 17px;
40 | color: #666;
41 | font-size: 14px;
42 | font-weight: 400;
43 | `;
44 |
--------------------------------------------------------------------------------
/backend/src/app/models/Membership.js:
--------------------------------------------------------------------------------
1 | import Sequelize, { Model } from 'sequelize';
2 | import { isBefore, isAfter } from 'date-fns';
3 |
4 | class Membership extends Model {
5 | static init(sequelize) {
6 | super.init(
7 | {
8 | start_date: Sequelize.DATE,
9 | end_date: Sequelize.DATE,
10 | price: Sequelize.DOUBLE,
11 | active: {
12 | type: Sequelize.VIRTUAL(Sequelize.BOOLEAN, [
13 | 'start_date',
14 | 'end_date',
15 | ]),
16 | get() {
17 | return (
18 | isBefore(this.get('start_date'), new Date()) &&
19 | isAfter(this.get('end_date'), new Date())
20 | );
21 | },
22 | },
23 | },
24 | {
25 | sequelize,
26 | }
27 | );
28 |
29 | return this;
30 | }
31 |
32 | static associate(models) {
33 | this.belongsTo(models.Student, { foreignKey: 'student_id', as: 'student' });
34 | this.belongsTo(models.Plan, { foreignKey: 'plan_id', as: 'plan' });
35 | }
36 | }
37 |
38 | export default Membership;
39 |
--------------------------------------------------------------------------------
/backend/src/lib/Queue.js:
--------------------------------------------------------------------------------
1 | import Bee from 'bee-queue';
2 | import ConfimationMail from '../app/jobs/ConfirmationMail';
3 | import HelpOrderAnswerMail from '../app/jobs/HelpOrderAnswerMail';
4 | import redisConfig from '../config/redis';
5 |
6 | const jobs = [ConfimationMail, HelpOrderAnswerMail];
7 |
8 | class Queue {
9 | constructor() {
10 | this.queues = {};
11 |
12 | this.init();
13 | }
14 |
15 | init() {
16 | jobs.forEach(({ key, handle }) => {
17 | this.queues[key] = {
18 | bee: new Bee(key, {
19 | redis: redisConfig,
20 | }),
21 | handle,
22 | };
23 | });
24 | }
25 |
26 | add(queue, job) {
27 | return this.queues[queue].bee.createJob(job).save();
28 | }
29 |
30 | processQueue() {
31 | jobs.forEach(job => {
32 | const { bee, handle } = this.queues[job.key];
33 |
34 | bee.on('failed', this.handleFailure).process(handle);
35 | });
36 | }
37 |
38 | handleFailure(job, err) {
39 | console.log(`Queue ${job.queue.name}: FAILED`, err);
40 | }
41 | }
42 |
43 | export default new Queue();
44 |
--------------------------------------------------------------------------------
/frontend/src/styles/global.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components';
2 |
3 | import 'react-toastify/dist/ReactToastify.css';
4 | import 'react-activity/dist/react-activity.css';
5 | import 'react-confirm-alert/src/react-confirm-alert.css';
6 | import 'react-datepicker/dist/react-datepicker.css';
7 |
8 | export default createGlobalStyle`
9 | @import url('https://fonts.googleapis.com/css?family=Roboto:400,700&display=swap');
10 |
11 | * {
12 | margin: 0;
13 | padding: 0;
14 | outline: 0;
15 | box-sizing: border-box;
16 | }
17 | *:focus {
18 | outline: 0;
19 | }
20 | html, body, #root {
21 | height: 100%;
22 | }
23 | body {
24 | text-rendering: optimizeLegibility !important;
25 | -webkit-font-smoothing: antialiased !important;
26 | background: #f5f5f5;
27 | }
28 | body, input, button, input::placeholder, textarea::placeholder {
29 | font: 14px 'Roboto', sans-serif;;
30 | }
31 | a {
32 | text-decoration: none;
33 | }
34 | ul {
35 | list-style: none;
36 | }
37 | button {
38 | cursor: pointer;
39 | }
40 | `;
41 |
--------------------------------------------------------------------------------
/mobile/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
13 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/backend/src/database/migrations/20191019173908-create-help-orders.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | up: (queryInterface, Sequelize) => {
3 | return queryInterface.createTable('orders', {
4 | id: {
5 | type: Sequelize.INTEGER,
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | },
10 | student_id: {
11 | type: Sequelize.INTEGER,
12 | references: { model: 'students', key: 'id' },
13 | onUpdate: 'CASCADE',
14 | onDelete: 'CASCADE',
15 | allowNull: true,
16 | },
17 | question: {
18 | type: Sequelize.STRING,
19 | allowNull: false,
20 | },
21 | answer: {
22 | type: Sequelize.STRING,
23 | },
24 | answer_at: {
25 | type: Sequelize.DATE,
26 | },
27 | created_at: {
28 | type: Sequelize.DATE,
29 | allowNull: false,
30 | },
31 | updated_at: {
32 | type: Sequelize.DATE,
33 | allowNull: false,
34 | },
35 | });
36 | },
37 |
38 | down: queryInterface => {
39 | return queryInterface.dropTable('orders');
40 | },
41 | };
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Stefano Akira Saffran
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/frontend/src/routes/Route.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Route, Redirect } from 'react-router-dom';
4 |
5 | import AuthLayout from '~/pages/_layouts/auth';
6 | import DefaultLayout from '~/pages/_layouts/default';
7 |
8 | import { store } from '~/store';
9 |
10 | const RouteWrapper = ({ component: Component, isPrivate, ...rest }) => {
11 | const { signed } = store.getState().auth;
12 |
13 | if (!signed && isPrivate) {
14 | return ;
15 | }
16 |
17 | if (signed && !isPrivate) {
18 | return ;
19 | }
20 |
21 | const Layout = signed ? DefaultLayout : AuthLayout;
22 |
23 | return (
24 | (
27 |
28 |
29 |
30 | )}
31 | />
32 | );
33 | };
34 |
35 | RouteWrapper.propTypes = {
36 | isPrivate: PropTypes.bool,
37 | component: PropTypes.oneOfType([PropTypes.element, PropTypes.func])
38 | .isRequired,
39 | };
40 |
41 | RouteWrapper.defaultProps = {
42 | isPrivate: false,
43 | };
44 |
45 | export default RouteWrapper;
46 |
--------------------------------------------------------------------------------
/mobile/android/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext {
5 | buildToolsVersion = "28.0.3"
6 | minSdkVersion = 16
7 | compileSdkVersion = 28
8 | targetSdkVersion = 28
9 | }
10 | repositories {
11 | google()
12 | jcenter()
13 | }
14 | dependencies {
15 | classpath("com.android.tools.build:gradle:3.4.2")
16 |
17 | // NOTE: Do not place your application dependencies here; they belong
18 | // in the individual module build.gradle files
19 | }
20 | }
21 |
22 | allprojects {
23 | repositories {
24 | mavenLocal()
25 | maven {
26 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
27 | url("$rootDir/../node_modules/react-native/android")
28 | }
29 | maven {
30 | // Android JSC is installed from npm
31 | url("$rootDir/../node_modules/jsc-android/dist")
32 | }
33 |
34 | google()
35 | jcenter()
36 | maven { url 'https://jitpack.io' }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/frontend/src/components/Header/Menu/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { MdMenu, MdClose } from 'react-icons/md';
3 |
4 | import useComponentVisible from '~/helpers/hooks/useComponentVisible';
5 | import LinkWrapper from '~/helpers/LinkWrapper';
6 |
7 | import { Container } from './styles';
8 |
9 | export default function Menu() {
10 | const {
11 | ref,
12 | isComponentVisible,
13 | setIsComponentVisible,
14 | } = useComponentVisible(false);
15 |
16 | return (
17 |
18 |
21 |
22 | {isComponentVisible ? (
23 | <>
24 |
25 | ALUNOS
26 | PLANOS
27 | MATRÍCULAS
28 | PEDIDOS DE AUXÍLIO
29 |
30 |
31 | >
32 | ) : null}
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/backend/src/database/migrations/20191016133943-create-students.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | up: (queryInterface, Sequelize) => {
3 | return queryInterface.createTable('students', {
4 | id: {
5 | type: Sequelize.INTEGER,
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | },
10 | name: {
11 | type: Sequelize.STRING,
12 | allowNull: false,
13 | },
14 | email: {
15 | type: Sequelize.STRING,
16 | allowNull: false,
17 | unique: true,
18 | },
19 | age: {
20 | type: Sequelize.INTEGER,
21 | allowNull: false,
22 | },
23 | weight: {
24 | type: Sequelize.FLOAT,
25 | allowNull: false,
26 | },
27 | height: {
28 | type: Sequelize.FLOAT,
29 | allowNull: false,
30 | },
31 | created_at: {
32 | type: Sequelize.DATE,
33 | allowNull: false,
34 | },
35 | updated_at: {
36 | type: Sequelize.DATE,
37 | allowNull: false,
38 | },
39 | });
40 | },
41 |
42 | down: queryInterface => {
43 | return queryInterface.dropTable('students');
44 | },
45 | };
46 |
--------------------------------------------------------------------------------
/frontend/src/assets/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/frontend/src/components/Pagination/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { darken } from 'polished';
3 |
4 | import colors from '~/styles/colors';
5 |
6 | export const Container = styled.div`
7 | margin-top: 15px;
8 | display: flex;
9 | flex-direction: row !important;
10 | justify-content: center !important;
11 | align-items: center;
12 |
13 | button:first-child {
14 | margin-right: 10px;
15 | }
16 | button:last-child {
17 | margin-left: 10px;
18 | }
19 |
20 | button {
21 | padding: 0 10px;
22 | height: 36px;
23 | line-height: 16px;
24 | border: 0;
25 | border-radius: 4px;
26 | color: ${colors.white};
27 | background: ${colors.primary};
28 |
29 | &:hover:not(:disabled) {
30 | background: ${darken(0.06, `${colors.primary}`)};
31 | }
32 |
33 | :disabled {
34 | opacity: 0.6;
35 | cursor: not-allowed;
36 | }
37 | }
38 |
39 | p {
40 | padding: 5px 10px;
41 | height: 36px;
42 | text-align: center;
43 | line-height: 26px;
44 | background: ${colors.white};
45 | border: 1px solid ${colors.lightGray};
46 | border-radius: 4px;
47 | color: ${colors.darkGray};
48 | margin: 0 10px;
49 | }
50 | `;
51 |
--------------------------------------------------------------------------------
/backend/src/app/jobs/ConfirmationMail.js:
--------------------------------------------------------------------------------
1 | import { format, parseISO } from 'date-fns';
2 | import pt from 'date-fns/locale/pt';
3 | import Mail from '../../lib/Mail';
4 |
5 | class ConfirmationMail {
6 | get key() {
7 | return 'ConfirmationMail';
8 | }
9 |
10 | async handle({ data }) {
11 | const { membershipInfo } = data;
12 |
13 | await Mail.sendMail({
14 | to: `${membershipInfo.student.name} <${membershipInfo.student.email}>`,
15 | subject: 'Matricula realizada com sucesso',
16 | template: 'confirmation',
17 | context: {
18 | student: membershipInfo.student.name,
19 | start_date: format(
20 | parseISO(membershipInfo.start_date),
21 | "dd 'de' MMMM 'de' yyyy",
22 | {
23 | locale: pt,
24 | }
25 | ),
26 | id: membershipInfo.student.id,
27 | plan: membershipInfo.plan.title,
28 | price: membershipInfo.price,
29 | duration: format(
30 | parseISO(membershipInfo.end_date),
31 | "'Até dia' dd 'de' MMMM 'de' yyyy",
32 | {
33 | locale: pt,
34 | }
35 | ),
36 | },
37 | });
38 | }
39 | }
40 |
41 | export default new ConfirmationMail();
42 |
--------------------------------------------------------------------------------
/backend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es6: true,
4 | node: true,
5 | jest: true,
6 | },
7 | extends: [
8 | 'airbnb-base', 'prettier'
9 | ],
10 | plugins: [ 'prettier', 'jest' ],
11 | globals: {
12 | Atomics: 'readonly',
13 | SharedArrayBuffer: 'readonly',
14 | },
15 | parserOptions: {
16 | ecmaVersion: 2018,
17 | sourceType: 'module',
18 | },
19 | rules: {
20 | "prettier/prettier": "error",
21 | "newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 }],
22 | "semi": 0,
23 | "eqeqeq": "error",
24 | "no-trailing-spaces": "error",
25 | "object-curly-spacing": [
26 | "error", "always"
27 | ],
28 | "arrow-spacing": [
29 | "error", { "before": true, "after": true }
30 | ],
31 | "no-console": 0,
32 | "react/prop-types": 0,
33 | "class-methods-use-this": "off",
34 | "no-param-reassign": "off",
35 | "camelcase": "off",
36 | "no-unused-vars": ["error", { "argsIgnorePattern": "next" }],
37 | "jest/no-disabled-tests": "warn",
38 | "jest/no-focused-tests": "error",
39 | "jest/no-identical-title": "error",
40 | "jest/prefer-to-have-length": "warn",
41 | "jest/valid-expect": "error"
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/backend/src/lib/Mail.js:
--------------------------------------------------------------------------------
1 | import nodemailer from 'nodemailer';
2 | import { resolve } from 'path';
3 | import exphbs from 'express-handlebars';
4 | import nodemailerhbs from 'nodemailer-express-handlebars';
5 | import mailConfig from '../config/mail';
6 |
7 | class Mail {
8 | constructor() {
9 | const { host, port, secure, auth } = mailConfig;
10 |
11 | this.transporter = nodemailer.createTransport({
12 | host,
13 | port,
14 | secure,
15 | auth: auth.user ? auth : null,
16 | });
17 |
18 | this.configureTemplates();
19 | }
20 |
21 | configureTemplates() {
22 | const viewPath = resolve(__dirname, '..', 'app', 'views', 'emails');
23 |
24 | this.transporter.use(
25 | 'compile',
26 | nodemailerhbs({
27 | viewEngine: exphbs.create({
28 | layoutsDir: resolve(viewPath, 'layouts'),
29 | partialsDir: resolve(viewPath, 'partials'),
30 | defaultLayout: 'default',
31 | extname: '.hbs',
32 | }),
33 | viewPath,
34 | extName: '.hbs',
35 | })
36 | );
37 | }
38 |
39 | sendMail(message) {
40 | return this.transporter.sendMail({
41 | ...mailConfig.default,
42 | ...message,
43 | });
44 | }
45 | }
46 |
47 | export default new Mail();
48 |
--------------------------------------------------------------------------------
/mobile/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es6: true,
4 | },
5 | extends: [
6 | 'airbnb',
7 | 'prettier',
8 | 'prettier/react'
9 | ],
10 | globals: {
11 | Atomics: 'readonly',
12 | SharedArrayBuffer: 'readonly',
13 | __DEV__: 'readonly',
14 | },
15 | parser: 'babel-eslint',
16 | parserOptions: {
17 | ecmaFeatures: {
18 | jsx: true,
19 | },
20 | ecmaVersion: 2018,
21 | sourceType: 'module',
22 | },
23 | plugins: [
24 | 'react',
25 | 'prettier',
26 | 'react-hooks'
27 | ],
28 | rules: {
29 | 'prettier/prettier': 'error',
30 | 'react/jsx-filename-extension': [
31 | 'warn',
32 | { extensions: ['.jsx', '.js' ] }
33 | ],
34 | 'import/prefer-default-export': 'off',
35 | "react/state-in-constructor": "off",
36 | 'react/static-property-placement':'off',
37 | 'react/jsx-props-no-spreading': 'off',
38 | 'no-param-reassign': 'off',
39 | 'no-console': ['error', { allow: ['tron'] }],
40 |
41 | 'react-hooks/rules-of-hooks': 'error',
42 | 'react-hooks/exhaustive-deps': 'warn',
43 | },
44 | settings: {
45 | "import/resolver": {
46 | "babel-plugin-root-import": {
47 | rootPathSuffix: "src"
48 | },
49 | },
50 | }
51 | };
52 |
--------------------------------------------------------------------------------
/mobile/src/components/Header/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Icon from 'react-native-vector-icons/MaterialIcons';
3 | import PropTypes from 'prop-types';
4 |
5 | import {
6 | Container,
7 | ContainerWithNav,
8 | Logo,
9 | LogoWrapper,
10 | BackButton,
11 | } from './styles';
12 |
13 | const HeaderLogo = ({ navigation }) => {
14 | return (
15 | <>
16 | {!navigation || navigation.state.routeName === 'HelpOrders' ? (
17 |
18 |
19 |
20 | ) : (
21 |
22 | {
24 | navigation.navigate('HelpOrders');
25 | }}
26 | >
27 |
28 |
29 |
30 |
31 |
32 |
33 | )}
34 | >
35 | );
36 | };
37 |
38 | HeaderLogo.propTypes = {
39 | navigation: PropTypes.shape({
40 | navigate: PropTypes.func,
41 | state: PropTypes.shape({
42 | routeName: PropTypes.string,
43 | }),
44 | }),
45 | };
46 |
47 | HeaderLogo.defaultProps = {
48 | navigation: null,
49 | };
50 |
51 | export default HeaderLogo;
52 |
--------------------------------------------------------------------------------
/mobile/src/components/Order/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components/native';
2 | import { RectButton } from 'react-native-gesture-handler';
3 |
4 | export const Container = styled.View`
5 | border: 1px solid #ddd;
6 | background: #fff;
7 | border-radius: 4px;
8 | margin: 0 0 10px;
9 | `;
10 |
11 | export const NewOrder = styled(RectButton)`
12 | flex-direction: column;
13 | padding: 20px 20px 25px;
14 | `;
15 |
16 | export const Top = styled.View`
17 | flex-direction: row;
18 | justify-content: space-between;
19 | align-items: center;
20 | `;
21 |
22 | export const Status = styled.View`
23 | flex-direction: row;
24 | justify-content: space-between;
25 | align-items: center;
26 | height: 17px;
27 | color: #999;
28 | font-size: 14px;
29 | font-weight: 700;
30 | `;
31 |
32 | export const StatusText = styled.Text`
33 | margin-left: 8px;
34 | font-size: 14px;
35 | font-weight: 700 bold;
36 | color: ${props => (props.answered ? '#42cb59' : '#999')};
37 | `;
38 |
39 | export const Time = styled.Text`
40 | color: #666;
41 | font-size: 14px;
42 | font-weight: 400;
43 | `;
44 |
45 | export const Question = styled.Text`
46 | margin-top: 15px;
47 | line-height: 26px;
48 | color: #666;
49 | font-size: 14px;
50 | font-weight: 400;
51 | `;
52 |
--------------------------------------------------------------------------------
/mobile/src/components/Order/index.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import Icon from 'react-native-vector-icons/MaterialIcons';
3 | import { parseISO, formatRelative } from 'date-fns';
4 | import pt from 'date-fns/locale/pt-BR';
5 |
6 | import {
7 | Container,
8 | NewOrder,
9 | Top,
10 | Status,
11 | StatusText,
12 | Time,
13 | Question,
14 | } from './styles';
15 |
16 | export default function Order({ data, onClick }) {
17 | const dateParsed = useMemo(() => {
18 | return formatRelative(parseISO(data.createdAt), new Date(), {
19 | locale: pt,
20 | addSuffix: true,
21 | });
22 | }, [data.createdAt]);
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
34 |
35 | {data.answer ? 'Respondido' : 'Sem resposta'}
36 |
37 |
38 |
39 |
40 | {data.question}
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/backend/src/app/controllers/SessionController.js:
--------------------------------------------------------------------------------
1 | import { object, string } from 'yup';
2 | import jwt from 'jsonwebtoken';
3 |
4 | import User from '../models/User';
5 | import authConfig from '../../config/auth';
6 |
7 | class SessionController {
8 | async store(req, res) {
9 | const schema = object().shape({
10 | email: string()
11 | .email()
12 | .required(),
13 | password: string().required(),
14 | });
15 |
16 | if (!(await schema.isValid(req.body))) {
17 | return res.status(400).json({ error: 'validation fails' });
18 | }
19 |
20 | const { email, password } = req.body;
21 |
22 | const user = await User.findOne({ where: { email } });
23 |
24 | if (!user) {
25 | return res.status(401).json({ error: 'User not found' });
26 | }
27 |
28 | if (!(await user.checkPassword(password))) {
29 | return res.status(401).json({ error: 'Password does not match' });
30 | }
31 |
32 | const { id, name } = user;
33 |
34 | return res.json({
35 | user: {
36 | id,
37 | name,
38 | email,
39 | },
40 | token: jwt.sign({ id }, authConfig.secret, {
41 | expiresIn: authConfig.expiresIn,
42 | }),
43 | });
44 | }
45 | }
46 |
47 | export default new SessionController();
48 |
--------------------------------------------------------------------------------
/mobile/src/store/modules/student/sagas.js:
--------------------------------------------------------------------------------
1 | import { takeLatest, call, all, put } from 'redux-saga/effects';
2 | import { showMessage } from 'react-native-flash-message';
3 |
4 | import api from '~/services/api';
5 | import { signInSuccess, signFailure, signOut } from './actions';
6 |
7 | export function* signIn({ payload }) {
8 | try {
9 | const { id } = payload;
10 | if (!id) {
11 | yield put(signFailure());
12 | return;
13 | }
14 | const { data } = yield call(api.get, `students/${id}/checkins`);
15 | yield put(signInSuccess(data.membership, data.student));
16 | } catch (err) {
17 | yield put(signFailure());
18 | showMessage({
19 | message: 'Falha ao realizar login',
20 | description: err.response
21 | ? err.response.data.error
22 | : 'Erro de conexão com o servidor',
23 | type: 'danger',
24 | });
25 | }
26 | }
27 |
28 | export function* verifyUser({ payload }) {
29 | const { student } = payload;
30 | try {
31 | yield call(api.get, `students/${student.profile.id}/checkins`);
32 | } catch (error) {
33 | if (error.response && error.response.status === 401) {
34 | yield put(signOut());
35 | }
36 | }
37 | }
38 |
39 | export default all([
40 | takeLatest('persist/REHYDRATE', verifyUser),
41 | takeLatest('@student/SIGN_IN_REQUEST', signIn),
42 | ]);
43 |
--------------------------------------------------------------------------------
/mobile/src/pages/HelpOrdersPages/Answer/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components/native';
2 |
3 | export const Container = styled.SafeAreaView`
4 | margin: 22px 20px;
5 | border: 1px solid #ddd;
6 | background: #fff;
7 | border-radius: 4px;
8 | `;
9 |
10 | export const Question = styled.View`
11 | padding: 20px 20px 25px;
12 | `;
13 |
14 | export const QuestionHeader = styled.View`
15 | flex-direction: row;
16 | justify-content: space-between;
17 | margin-bottom: 15px;
18 | `;
19 |
20 | export const QuestionText = styled.Text`
21 | color: #444;
22 | font-size: 14px;
23 | font-weight: 700 bold;
24 | text-transform: uppercase;
25 | `;
26 |
27 | export const Time = styled.Text`
28 | color: #666;
29 | font-size: 14px;
30 | font-weight: 400;
31 | `;
32 |
33 | export const QuestionBody = styled.Text`
34 | line-height: 26px;
35 | color: #666;
36 | font-size: 14px;
37 | font-weight: 400;
38 | `;
39 |
40 | export const AnswerWrapper = styled.View`
41 | padding: 0 20px 27px;
42 | `;
43 |
44 | export const AnswerHeader = styled.Text`
45 | margin-bottom: 15px;
46 | color: #444;
47 | font-size: 14px;
48 | font-weight: 700 bold;
49 | text-transform: uppercase;
50 | `;
51 |
52 | export const AnswerBody = styled.Text`
53 | line-height: 26px;
54 | color: #666;
55 | font-size: 14px;
56 | font-weight: 400;
57 | `;
58 |
--------------------------------------------------------------------------------
/mobile/src/pages/SignIn/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Image } from 'react-native';
3 | import { useDispatch, useSelector } from 'react-redux';
4 |
5 | import logo from '~/assets/8-layers.png';
6 |
7 | import Background from '~/components/Background/BackgroundSignIn';
8 | import { signInRequest } from '~/store/modules/student/actions';
9 |
10 | import { Container, Form, FormInput, SubmitButton } from './styles';
11 |
12 | export default function SignIn() {
13 | const [id, setId] = useState('');
14 |
15 | const dispatch = useDispatch();
16 |
17 | const loading = useSelector(state => state.student.loading);
18 |
19 | const handleSubmit = () => {
20 | dispatch(signInRequest(id));
21 | };
22 | return (
23 |
24 |
25 |
26 |
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/frontend/src/components/Pagination/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {
4 | FaAngleDoubleLeft,
5 | FaAngleDoubleRight,
6 | FaAngleRight,
7 | FaAngleLeft,
8 | } from 'react-icons/fa';
9 |
10 | import { Container } from './styles';
11 |
12 | export default function Pagination({ page, totalPages, setPage }) {
13 | return (
14 |
15 |
18 |
25 | {page}
26 |
33 |
40 |
41 | );
42 | }
43 |
44 | Pagination.propTypes = {
45 | page: PropTypes.number.isRequired,
46 | totalPages: PropTypes.number.isRequired,
47 | setPage: PropTypes.func.isRequired,
48 | };
49 |
--------------------------------------------------------------------------------
/backend/src/database/migrations/20191018173726-membership-management.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | up: (queryInterface, Sequelize) => {
3 | return queryInterface.createTable('memberships', {
4 | id: {
5 | type: Sequelize.INTEGER,
6 | allowNull: false,
7 | autoIncrement: true,
8 | primaryKey: true,
9 | },
10 | student_id: {
11 | type: Sequelize.INTEGER,
12 | references: { model: 'students', key: 'id' },
13 | onUpdate: 'CASCADE',
14 | onDelete: 'CASCADE',
15 | allowNull: true,
16 | },
17 | plan_id: {
18 | type: Sequelize.INTEGER,
19 | references: { model: 'plans', key: 'id' },
20 | onUpdate: 'CASCADE',
21 | onDelete: 'SET NULL',
22 | allowNull: true,
23 | },
24 | start_date: {
25 | type: Sequelize.DATE,
26 | allowNull: false,
27 | },
28 | end_date: {
29 | type: Sequelize.DATE,
30 | allowNull: false,
31 | },
32 | price: {
33 | type: Sequelize.DOUBLE,
34 | allowNull: false,
35 | },
36 | created_at: {
37 | type: Sequelize.DATE,
38 | allowNull: false,
39 | },
40 | updated_at: {
41 | type: Sequelize.DATE,
42 | allowNull: false,
43 | },
44 | });
45 | },
46 |
47 | down: queryInterface => {
48 | return queryInterface.dropTable('memberships');
49 | },
50 | };
51 |
--------------------------------------------------------------------------------
/frontend/src/components/HeightInput/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import NumberFormat from 'react-number-format';
3 | import { useField } from '@rocketseat/unform';
4 | import PropTypes from 'prop-types';
5 |
6 | export default function Height({ name, setChange }) {
7 | const ref = useRef();
8 | const { fieldName, defaultValue, registerField, error } = useField(name);
9 | const [value, setValue] = useState(defaultValue);
10 |
11 | useEffect(() => {
12 | if (ref.current) {
13 | registerField({
14 | name: fieldName,
15 | ref: ref.current,
16 | path: 'props.value',
17 | });
18 | }
19 | }, [ref, fieldName]); // eslint-disable-line
20 |
21 | return (
22 | <>
23 | {
33 | setValue(values.floatValue);
34 | if (setChange) {
35 | setChange(values.floatValue);
36 | }
37 | }}
38 | />
39 | {error && {error}}
40 | >
41 | );
42 | }
43 |
44 | Height.defaultProps = {
45 | setChange: null,
46 | };
47 |
48 | Height.propTypes = {
49 | name: PropTypes.string.isRequired,
50 | setChange: PropTypes.func,
51 | };
52 |
--------------------------------------------------------------------------------
/mobile/src/pages/HelpOrdersPages/Answer/index.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { parseISO, formatRelative } from 'date-fns';
3 | import pt from 'date-fns/locale/pt-BR';
4 | import PropTypes from 'prop-types';
5 |
6 | import Background from '~/components/Background';
7 | import {
8 | Container,
9 | Question,
10 | QuestionHeader,
11 | QuestionText,
12 | Time,
13 | QuestionBody,
14 | AnswerWrapper,
15 | AnswerHeader,
16 | AnswerBody,
17 | } from './styles';
18 |
19 | export default function Answer({ navigation }) {
20 | const order = navigation.getParam('order');
21 |
22 | const dateParsed = useMemo(() => {
23 | return formatRelative(parseISO(order.createdAt), new Date(), {
24 | locale: pt,
25 | addSuffix: true,
26 | });
27 | }, [order.createdAt]);
28 |
29 | return (
30 |
31 |
32 |
33 |
34 | pergunta
35 |
36 |
37 | {order.question}
38 |
39 |
40 | resposta
41 | {order.answer}
42 |
43 |
44 |
45 | );
46 | }
47 |
48 | Answer.propTypes = {
49 | navigation: PropTypes.shape({
50 | getParam: PropTypes.func,
51 | }).isRequired,
52 | };
53 |
--------------------------------------------------------------------------------
/frontend/src/components/Modal/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useImperativeHandle } from 'react';
2 | import PropTypes from 'prop-types';
3 | import useComponentVisible from '~/helpers/hooks/useComponentVisible';
4 |
5 | import { Container } from './styles';
6 |
7 | const Modal = React.forwardRef((props, forwardRef) => {
8 | const {
9 | ref,
10 | isComponentVisible,
11 | setIsComponentVisible,
12 | } = useComponentVisible(false);
13 | const [answer, setAnswer] = useState('');
14 |
15 | const showHidestyle = { display: isComponentVisible ? '' : 'none' };
16 |
17 | useImperativeHandle(forwardRef, () => {
18 | return {
19 | setIsComponentVisible,
20 | };
21 | });
22 |
23 | return (
24 |
25 |
26 | {props.children}
27 |
28 | SUA RESPOSTA
29 |
36 |
39 |
40 |
41 | );
42 | });
43 |
44 | Modal.propTypes = {
45 | handleAnswer: PropTypes.func.isRequired,
46 | children: PropTypes.element.isRequired,
47 | };
48 |
49 | export default Modal;
50 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/frontend/src/routes/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Switch } from 'react-router-dom';
3 | import Route from './Route';
4 |
5 | import SignIn from '~/pages/SignIn';
6 | import Students from '~/pages/Students';
7 | import ManageStudent from '~/pages/Students/ManageStudent';
8 | import Plans from '~/pages/Plans';
9 | import ManagePlans from '~/pages/Plans/ManagePlans';
10 | import Memberships from '~/pages/Memberships';
11 | import ManageMembership from '~/pages/Memberships/ManageMembership';
12 | import HelpOrders from '~/pages/HelpOrders';
13 |
14 | const Routes = () => {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default Routes;
41 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/auth/sagas.js:
--------------------------------------------------------------------------------
1 | import { takeLatest, call, all, put } from 'redux-saga/effects';
2 | import { toast } from 'react-toastify';
3 | import decode from 'jwt-decode';
4 |
5 | import history from '~/services/history';
6 | import api from '~/services/api';
7 | import { signInSuccess, signFailure, tokenInvalid } from './actions';
8 |
9 | export function* signIn({ payload }) {
10 | try {
11 | const { email, password } = payload;
12 |
13 | const response = yield call(api.post, 'sessions', {
14 | email,
15 | password,
16 | });
17 |
18 | const { token, user } = response.data;
19 |
20 | api.defaults.headers.Authorization = `Bearer ${token}`;
21 |
22 | yield put(signInSuccess(token, user));
23 |
24 | history.push('/students');
25 | } catch (err) {
26 | yield put(signFailure());
27 | toast.error(
28 | (err.response && err.response.data.error) || 'Error connecting to server'
29 | );
30 | }
31 | }
32 |
33 | export function* setToken({ payload }) {
34 | if (!payload) return;
35 |
36 | const { token } = payload.auth;
37 |
38 | if (token) {
39 | const decoded = decode(token);
40 |
41 | if (decoded.exp < Date.now() / 1000) {
42 | yield put(tokenInvalid());
43 | }
44 | }
45 |
46 | api.defaults.headers.Authorization = `Bearer ${token}`;
47 | }
48 |
49 | export function signOut() {
50 | history.push('/');
51 | }
52 |
53 | export default all([
54 | takeLatest('persist/REHYDRATE', setToken),
55 | takeLatest('@auth/SIGN_IN_REQUEST', signIn),
56 | takeLatest('@auth/SIGN_OUT', signOut),
57 | ]);
58 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gympoint",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "dev": "nodemon src/server.js",
8 | "queue": "nodemon src/queue.js",
9 | "dev:debug": "nodemon --inspect src/server.js",
10 | "pretest": "NODE_ENV=test sequelize db:migrate",
11 | "test": "NODE_ENV=test jest --runInBand --forceExit",
12 | "posttest": "NODE_ENV=test sequelize db:migrate:undo:all"
13 | },
14 | "dependencies": {
15 | "bcryptjs": "^2.4.3",
16 | "bee-queue": "^1.2.2",
17 | "cors": "^2.8.5",
18 | "date-fns": "^2.5.1",
19 | "dotenv": "^8.2.0",
20 | "express": "^4.17.1",
21 | "express-handlebars": "^3.1.0",
22 | "jsonwebtoken": "^8.5.1",
23 | "nodemailer": "^6.4.16",
24 | "nodemailer-express-handlebars": "^3.1.0",
25 | "pg": "^7.12.1",
26 | "pg-hstore": "^2.3.3",
27 | "sequelize": "^5.19.6",
28 | "socket.io": "^2.3.0",
29 | "yup": "^0.28.0"
30 | },
31 | "devDependencies": {
32 | "@sucrase/jest-plugin": "2.0.0",
33 | "@types/jest": "25.1.4",
34 | "eslint": "6.8.0",
35 | "eslint-config-airbnb-base": "14.1.0",
36 | "eslint-config-prettier": "6.10.0",
37 | "eslint-plugin-import": "2.20.1",
38 | "eslint-plugin-jest": "23.8.2",
39 | "eslint-plugin-prettier": "3.1.2",
40 | "factory-girl": "5.0.4",
41 | "faker": "4.1.0",
42 | "jest": "25.1.0",
43 | "nodemon": "2.0.2",
44 | "prettier": "1.19.1",
45 | "sequelize-cli": "5.5.1",
46 | "sqlite3": "4.1.1",
47 | "sucrase": "3.12.1",
48 | "supertest": "4.0.2"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/backend/src/app.js:
--------------------------------------------------------------------------------
1 | import './bootstrap';
2 |
3 | import express from 'express';
4 | import cors from 'cors';
5 | import io from 'socket.io';
6 | import http from 'http';
7 |
8 | import routes from './routes';
9 |
10 | import './database';
11 |
12 | class App {
13 | constructor() {
14 | this.app = express();
15 | this.server = http.Server(this.app);
16 |
17 | this.socket();
18 | this.middlewares();
19 | this.routes();
20 |
21 | this.connectedUsers = {};
22 | this.admin = [];
23 | }
24 |
25 | socket() {
26 | this.io = io(this.server);
27 |
28 | this.io.on('connection', socket => {
29 | const { user_id, origin = 'mobile' } = socket.handshake.query;
30 |
31 | if (origin === 'mobile') {
32 | this.connectedUsers[user_id] = socket.id;
33 | } else {
34 | this.admin.push(socket.id);
35 | socket.join('admin');
36 | }
37 |
38 | socket.on('disconnect', () => {
39 | if (origin === 'mobile') {
40 | delete this.connectedUsers[user_id];
41 | } else {
42 | this.admin.splice(socket.id, 1);
43 | socket.leave('admin');
44 | }
45 | });
46 | });
47 | }
48 |
49 | middlewares() {
50 | this.app.use(cors());
51 | this.app.use(express.json());
52 |
53 | this.app.use((req, res, next) => {
54 | req.io = this.io;
55 | req.connectedUsers = this.connectedUsers;
56 | req.admin = this.admin;
57 |
58 | next();
59 | });
60 | }
61 |
62 | routes() {
63 | this.app.use(routes);
64 | }
65 | }
66 |
67 | export default new App().server;
68 |
--------------------------------------------------------------------------------
/frontend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es6: true,
5 | jest: true,
6 | },
7 | extends: [
8 | 'airbnb',
9 | 'prettier',
10 | 'prettier/react'
11 | ],
12 | globals: {
13 | Atomics: 'readonly',
14 | SharedArrayBuffer: 'readonly',
15 | __DEV__: 'readonly',
16 | },
17 | parser: 'babel-eslint',
18 | parserOptions: {
19 | ecmaFeatures: {
20 | jsx: true,
21 | },
22 | ecmaVersion: 2018,
23 | sourceType: 'module',
24 | },
25 | plugins: [
26 | 'react',
27 | 'prettier',
28 | 'react-hooks'
29 | ],
30 | rules: {
31 | 'prettier/prettier': 'error',
32 | 'react/jsx-filename-extension': [
33 | 'warn',
34 | { extensions: ['.jsx', '.js' ] }
35 | ],
36 | 'import/prefer-default-export': 'off',
37 | "react/state-in-constructor": "off",
38 | "no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
39 | 'react/static-property-placement':'off',
40 | 'react/jsx-props-no-spreading': 'off',
41 | "react/jsx-one-expression-per-line": "off",
42 | "global-require": "off",
43 | "react-native/no-raw-text": "off",
44 | 'no-param-reassign': 'off',
45 | "no-underscore-dangle": "off",
46 | camelcase: "off",
47 | 'no-console': ['error', { allow: ['tron'] }],
48 |
49 | 'react-hooks/rules-of-hooks': 'error',
50 | 'react-hooks/exhaustive-deps': 'warn',
51 | 'jsx-a11y/label-has-associated-control': 0,
52 |
53 | },
54 | settings: {
55 | "import/resolver": {
56 | "babel-plugin-root-import": {
57 | rootPathSuffix: "src"
58 | },
59 | },
60 | },
61 | };
62 |
--------------------------------------------------------------------------------
/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/actions';
7 |
8 | import Loading from '~/components/Loading';
9 |
10 | import logo from '~/assets/logo.svg';
11 |
12 | const schema = Yup.object().shape({
13 | email: Yup.string()
14 | .email('Insira um e-mail válido')
15 | .required('O e-mail é obrigatório'),
16 | password: Yup.string().required('A senha é obrigatória'),
17 | });
18 |
19 | export default function SignIn() {
20 | const dispatch = useDispatch();
21 | const loading = useSelector(state => state.auth.loading);
22 |
23 | const handleSubmit = ({ email, password }) => {
24 | dispatch(signInRequest(email, password));
25 | };
26 |
27 | return (
28 | <>
29 |
30 |
31 |
56 | >
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/frontend/src/components/Header/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { darken } from 'polished';
3 |
4 | export const Container = styled.div`
5 | background: #fff;
6 | padding: 0 30px;
7 | min-width: 550px;
8 | `;
9 |
10 | export const Content = styled.div`
11 | height: 64px;
12 | max-width: 1440px;
13 | margin: 0 auto;
14 | display: flex;
15 | justify-content: space-between;
16 | align-items: center;
17 |
18 | nav {
19 | display: flex;
20 | align-items: center;
21 | width: 70%;
22 |
23 | img {
24 | width: 160.679px;
25 | height: 23.636px;
26 | margin-right: 30px;
27 | padding-right: 30px;
28 | border-right: 1px solid #ddd;
29 | }
30 |
31 | a {
32 | font-weight: bold;
33 | color: #999;
34 | font-size: 15px;
35 | margin-right: 20px;
36 |
37 | &:hover {
38 | color: ${darken(0.3, '#999')};
39 | }
40 | }
41 | }
42 |
43 | aside {
44 | display: flex;
45 | align-items: center;
46 | }
47 |
48 | @media (max-width: 768px) {
49 | nav {
50 | img {
51 | margin: 0;
52 | padding: 0;
53 | border: 0;
54 | margin-left: 56px;
55 | padding-left: 30px;
56 | border-left: 1px solid #ddd;
57 | }
58 | }
59 | }
60 | `;
61 |
62 | export const Profile = styled.div`
63 | display: flex;
64 | flex-direction: column;
65 | align-items: flex-end;
66 |
67 | strong {
68 | color: #666;
69 | margin-bottom: 4px;
70 | }
71 |
72 | button {
73 | color: #de3b3b;
74 | font-size: 14px;
75 | background: none;
76 | border: none;
77 |
78 | &:hover {
79 | color: ${darken(0.1, '#de3b3b')};
80 | }
81 | }
82 | `;
83 |
--------------------------------------------------------------------------------
/frontend/src/components/Header/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useLayoutEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import LinkWrapper from '~/helpers/LinkWrapper';
4 | import Menu from './Menu';
5 |
6 | import { Container, Content, Profile } from './styles';
7 |
8 | import logo from '~/assets/logo-header.png';
9 | import { signOut } from '~/store/modules/auth/actions';
10 |
11 | export default function Header() {
12 | const [width, setWidth] = useState([0]);
13 | const profile = useSelector(state => state.user.profile);
14 | const dispatch = useDispatch();
15 |
16 | useLayoutEffect(() => {
17 | function updateWidth() {
18 | setWidth([window.innerWidth]);
19 | }
20 | window.addEventListener('resize', updateWidth);
21 | updateWidth();
22 | return () => window.removeEventListener('resize', updateWidth);
23 | }, []);
24 |
25 | const handleSignout = () => {
26 | dispatch(signOut());
27 | };
28 |
29 | return (
30 |
31 |
32 | {width < 768 ? (
33 |
37 | ) : (
38 |
45 | )}
46 |
47 |
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/frontend/src/pages/_layouts/auth/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { darken } from 'polished';
3 | import colors from '~/styles/colors';
4 |
5 | export const Wrapper = styled.div`
6 | height: 100%;
7 | background: ${colors.primary};
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | `;
12 |
13 | export const Content = styled.div`
14 | width: 100%;
15 | max-width: 350px;
16 | border-radius: 4px;
17 | text-align: center;
18 | box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.2);
19 | background-color: ${colors.white};
20 | align-items: center;
21 | padding: 35px 20px;
22 |
23 | img {
24 | margin-top: 10px;
25 | }
26 |
27 | form {
28 | display: flex;
29 | flex-direction: column;
30 | margin-top: 25px;
31 | padding: 0 8px;
32 |
33 | label + label {
34 | margin-top: 10px;
35 | }
36 |
37 | label {
38 | display: flex;
39 | flex-direction: column;
40 | align-items: flex-start;
41 | font-weight: bold;
42 | color: ${colors.darkGray};
43 | margin: 0 0 10px;
44 |
45 | input {
46 | width: 100%;
47 | height: 44px;
48 | padding: 0 15px;
49 | margin-top: 5px;
50 | border-radius: 4px;
51 | border: 1px solid ${colors.border};
52 | }
53 |
54 | span {
55 | margin-top: 10px;
56 | font-weight: bold;
57 | color: ${darken(0.08, `${colors.primary}`)};
58 | }
59 | }
60 | button {
61 | height: 44px;
62 | background: ${colors.primary};
63 | border: 0;
64 | border-radius: 4px;
65 | font-weight: bold;
66 | color: ${colors.white};
67 | margin: 5px 0 10px;
68 | font-size: 15px;
69 | transition: background 0.2s;
70 |
71 | &:hover {
72 | background: ${darken(0.03, `${colors.primary}`)};
73 | }
74 | }
75 | }
76 | `;
77 |
--------------------------------------------------------------------------------
/mobile/src/pages/ProfilePage/Profile/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components/native';
2 |
3 | import Button from '~/components/Button';
4 |
5 | export const Container = styled.SafeAreaView`
6 | align-self: stretch;
7 | flex: 1;
8 | `;
9 |
10 | export const MembershipInfo = styled.ScrollView.attrs({
11 | showsVerticalScrollIndicator: false,
12 | })`
13 | padding: 0 30px;
14 | `;
15 |
16 | export const Title = styled.Text`
17 | font-size: 20px;
18 | color: #000;
19 | font-weight: bold;
20 | align-self: center;
21 | margin-top: 30px;
22 | `;
23 |
24 | export const StudentName = styled.View`
25 | margin-top: 30px;
26 | flex-direction: row;
27 | align-items: baseline;
28 | `;
29 |
30 | export const StudentEmail = styled.View`
31 | margin-top: 15px;
32 | flex-direction: row;
33 | align-items: baseline;
34 | `;
35 |
36 | export const Plan = styled.View`
37 | margin-top: 15px;
38 | flex-direction: row;
39 | align-items: baseline;
40 | `;
41 |
42 | export const Membership = styled.View`
43 | margin-top: 15px;
44 | flex-direction: row;
45 | align-items: baseline;
46 | `;
47 |
48 | export const Active = styled.View`
49 | flex-direction: row;
50 | align-items: baseline;
51 | `;
52 |
53 | export const Until = styled.Text`
54 | margin-left: 10px;
55 | font-size: 13px;
56 | color: #999;
57 | `;
58 |
59 | export const Label = styled.Text`
60 | font-size: 15px;
61 | font-weight: bold;
62 | padding-right: 10px;
63 | `;
64 |
65 | export const Name = styled.Text`
66 | font-size: 14px;
67 | font-weight: bold;
68 | color: #666;
69 | `;
70 |
71 | export const Email = styled.Text`
72 | font-size: 14px;
73 | font-weight: bold;
74 | color: #666;
75 | `;
76 |
77 | export const PlanTitle = styled.Text`
78 | font-size: 14px;
79 | font-weight: bold;
80 | color: #666;
81 | `;
82 |
83 | export const LogoutButton = styled(Button)`
84 | margin-top: 20px;
85 | `;
86 |
--------------------------------------------------------------------------------
/backend/src/app/controllers/AnswerController.js:
--------------------------------------------------------------------------------
1 | import { object, string } from 'yup';
2 | import Order from '../models/Order';
3 | import Student from '../models/Student';
4 |
5 | import HelpOrderAnswerMail from '../jobs/HelpOrderAnswerMail';
6 | import Queue from '../../lib/Queue';
7 |
8 | class AnswerController {
9 | async store(req, res) {
10 | const schema = object().shape({
11 | answer: string().required(),
12 | });
13 |
14 | if (!(await schema.isValid(req.body))) {
15 | return res.status(400).json({ error: 'validation fails' });
16 | }
17 |
18 | const { answer } = req.body;
19 |
20 | const order = await Order.findByPk(req.params.id, {
21 | include: [
22 | {
23 | model: Student,
24 | as: 'student',
25 | attributes: ['id', 'name', 'email'],
26 | },
27 | ],
28 | });
29 |
30 | if (order && order.answer) {
31 | return res.status(401).json({
32 | error: 'This order was already answered',
33 | });
34 | }
35 |
36 | order.answer = answer;
37 | order.answer_at = new Date();
38 |
39 | await order.save();
40 |
41 | const ownerSocket = req.connectedUsers[order.student.id];
42 |
43 | if (ownerSocket) {
44 | req.io.to(ownerSocket).emit('order_response', order);
45 | }
46 |
47 | if (!ownerSocket && process.env.NODE_ENV !== 'test') {
48 | await Queue.add(HelpOrderAnswerMail.key, {
49 | order,
50 | });
51 | }
52 |
53 | return res.json(order);
54 | }
55 |
56 | async index(req, res) {
57 | const orders = await Order.findAll({
58 | where: {
59 | answer: null,
60 | },
61 | include: [
62 | {
63 | model: Student,
64 | as: 'student',
65 | attributes: ['name'],
66 | },
67 | ],
68 | });
69 |
70 | return res.json(orders);
71 | }
72 | }
73 |
74 | export default new AnswerController();
75 |
--------------------------------------------------------------------------------
/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 | MaterialIcons.ttf
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/frontend/src/components/Header/Menu/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { darken } from 'polished';
3 |
4 | export const Container = styled.div`
5 | display: flex;
6 | flex-direction: column;
7 |
8 | button {
9 | background: none;
10 | border: 0;
11 | position: absolute;
12 | top: 19px;
13 | }
14 |
15 | div {
16 | display: flex;
17 | flex-direction: column;
18 | background: rgb(255, 255, 255);
19 | padding: 15px 5px;
20 | position: absolute;
21 | top: 64px;
22 | left: 0;
23 | height: 92.9%;
24 | z-index: 2;
25 | animation: ${props =>
26 | props.visible &&
27 | 'modalFadeIn .3s cubic-bezier(0.165, 0.840, 0.440, 1.000) forwards'};
28 |
29 | a {
30 | font-weight: bold;
31 | color: #999;
32 | font-size: 15px;
33 | padding: 20px;
34 | animation: ${props =>
35 | props.visible &&
36 | 'modalLinksFadeIn .8s cubic-bezier(0.165, 0.840, 0.440, 1.000) forwards'};
37 |
38 | &:hover {
39 | color: ${darken(0.3, '#999')};
40 | }
41 | }
42 | }
43 |
44 | div + div {
45 | position: fixed;
46 | width: 100vw;
47 | height: 100vh;
48 | top: 64px;
49 | left: 0;
50 | z-index: 1;
51 | opacity: 0.8;
52 | transform: scale(1);
53 | animation: ${props =>
54 | props.visible &&
55 | 'fadeIn 1s cubic-bezier(0.165, 0.840, 0.440, 1.000) forwards'};
56 | }
57 |
58 | @keyframes fadeIn {
59 | 0% {
60 | background: rgba(0, 0, 0, 0);
61 | }
62 | 100% {
63 | background: rgba(0, 0, 0, 0.8);
64 | }
65 | }
66 |
67 | @keyframes modalFadeIn {
68 | 0% {
69 | background: rgb(255, 255, 255, 0);
70 | }
71 | 100% {
72 | background: rgb(255, 255, 255);
73 | }
74 | }
75 | @keyframes modalLinksFadeIn {
76 | 0% {
77 | opacity: 0;
78 | }
79 | 100% {
80 | opacity: 1;
81 | }
82 | }
83 | `;
84 |
--------------------------------------------------------------------------------
/mobile/src/pages/HelpOrdersPages/NewOrder/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import PropTypes from 'prop-types';
4 | import { showMessage } from 'react-native-flash-message';
5 |
6 | import api from '~/services/api';
7 |
8 | import Background from '~/components/Background';
9 | import { Container, OrderTextArea, SubmitButton } from './styles';
10 |
11 | export default function NewOrder({ navigation }) {
12 | const [question, setQuestion] = useState('');
13 | const id = useSelector(state => state.student.profile.id);
14 |
15 | const handleSubmit = async () => {
16 | try {
17 | await api.post(`students/${id}/help-orders`, { question });
18 |
19 | showMessage({
20 | message: 'Pedido enviado',
21 | description: 'Um instrutor entrará em contato em breve.',
22 | type: 'info',
23 | });
24 |
25 | setQuestion('');
26 | navigation.navigate('HelpOrders');
27 | } catch (err) {
28 | showMessage({
29 | message: 'Falha ao realizar o pedido',
30 | description: err.response
31 | ? err.response.data.error
32 | : 'Erro de conexão com o servidor',
33 | type: 'danger',
34 | });
35 | }
36 | };
37 |
38 | return (
39 |
40 |
41 |
53 | Enviar pedido
54 |
55 |
56 | );
57 | }
58 |
59 | NewOrder.propTypes = {
60 | navigation: PropTypes.shape({
61 | navigate: PropTypes.func,
62 | }).isRequired,
63 | };
64 |
--------------------------------------------------------------------------------
/frontend/src/pages/HelpOrders/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { darken } from 'polished';
3 |
4 | import colors from '~/styles/colors';
5 |
6 | export const Container = styled.div`
7 | width: 100%;
8 | max-width: 700px;
9 | min-width: 550px;
10 | margin: 30px auto;
11 | padding: 0 2px;
12 |
13 | div {
14 | display: flex;
15 | justify-content: center;
16 | flex-direction: column;
17 | align-items: center;
18 | margin-bottom: 10px;
19 |
20 | h1 {
21 | color: ${colors.darkGray};
22 | font-size: 24px;
23 | }
24 | }
25 |
26 | @media (min-width: 768px) {
27 | div {
28 | flex-direction: row;
29 | justify-content: space-between;
30 | align-items: center;
31 | margin-bottom: 10px;
32 | }
33 | }
34 | `;
35 |
36 | export const EmptyList = styled.div`
37 | margin-top: 20px;
38 | padding: 30px;
39 | width: 100%;
40 | background: ${colors.white};
41 | flex-direction: column !important;
42 |
43 | p {
44 | margin-top: 20px;
45 | color: ${colors.darkGray};
46 | }
47 | `;
48 |
49 | export const HelpOrdersList = styled.ul`
50 | margin-top: 20px;
51 | padding: 30px;
52 | width: 100%;
53 | background: ${colors.white};
54 |
55 | li {
56 | display: grid;
57 | grid-template-columns: 4fr 1fr;
58 | padding-bottom: 15px;
59 |
60 | strong {
61 | color: ${colors.darkGray};
62 | font-weight: bold;
63 | font-size: 16px;
64 | }
65 |
66 | span {
67 | height: 0;
68 | font-size: 16px;
69 | color: ${colors.gray};
70 | }
71 |
72 | button {
73 | background: none;
74 | font-size: 15px;
75 | border: 0;
76 | color: ${colors.editButton};
77 | padding-left: 40px;
78 |
79 | &:hover {
80 | color: ${darken(0.1, `${colors.editButton}`)};
81 | }
82 | }
83 | }
84 |
85 | li + li {
86 | border-bottom: 1px solid ${colors.lightBorder};
87 | margin-bottom: 15px;
88 | }
89 |
90 | li:last-child {
91 | border: 0;
92 | padding-bottom: 0;
93 | margin-bottom: 0;
94 | }
95 | `;
96 |
--------------------------------------------------------------------------------
/mobile/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "challenge-10",
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 | "test": "jest",
10 | "lint": "eslint ."
11 | },
12 | "dependencies": {
13 | "@react-native-community/async-storage": "1.8.1",
14 | "axios": "0.21.1",
15 | "immer": "8.0.1",
16 | "date-fns": "2.11.0",
17 | "lodash.debounce": "4.0.8",
18 | "prop-types": "15.7.2",
19 | "react": "16.14.0",
20 | "react-native": "0.61.5",
21 | "react-native-flash-message": "0.1.15",
22 | "react-native-gesture-handler": "1.6.0",
23 | "react-native-reanimated": "1.7.0",
24 | "react-native-screens": "2.18.1",
25 | "react-native-vector-icons": "6.6.0",
26 | "react-navigation": "4.4.4",
27 | "react-navigation-stack": "2.10.4",
28 | "react-navigation-tabs": "2.11.1",
29 | "react-redux": "7.2.0",
30 | "reactotron-react-native": "4.0.3",
31 | "reactotron-redux": "3.1.2",
32 | "reactotron-redux-saga": "4.2.3",
33 | "redux": "4.0.5",
34 | "redux-persist": "6.0.0",
35 | "redux-saga": "1.1.3",
36 | "socket.io-client": "2.3.0",
37 | "styled-components": "5.0.1"
38 | },
39 | "devDependencies": {
40 | "@babel/core": "7.8.7",
41 | "@babel/runtime": "7.8.7",
42 | "@react-native-community/eslint-config": "0.0.7",
43 | "babel-eslint": "10.1.0",
44 | "babel-jest": "25.1.0",
45 | "babel-plugin-root-import": "6.4.1",
46 | "eslint": "6.8.0",
47 | "eslint-config-airbnb": "18.1.0",
48 | "eslint-config-prettier": "6.10.0",
49 | "eslint-import-resolver-babel-plugin-root-import": "1.1.1",
50 | "eslint-plugin-import": "2.20.1",
51 | "eslint-plugin-jsx-a11y": "6.2.3",
52 | "eslint-plugin-prettier": "3.1.2",
53 | "eslint-plugin-react": "7.19.0",
54 | "eslint-plugin-react-hooks": "2.5.1",
55 | "jest": "25.1.0",
56 | "metro-react-native-babel-preset": "0.66.2",
57 | "prettier": "1.19.1",
58 | "react-test-renderer": "16.14.0"
59 | },
60 | "jest": {
61 | "preset": "react-native"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/frontend/src/pages/Plans/ManagePlans/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { darken } from 'polished';
3 |
4 | import colors from '~/styles/colors';
5 |
6 | export const Container = styled.div`
7 | width: 100%;
8 | max-width: 1080px;
9 | min-width: 550px;
10 | margin: 30px auto;
11 |
12 | form {
13 | width: 100%;
14 | display: flex;
15 | flex-direction: column;
16 | padding: 5px 30px 30px;
17 | background: ${colors.white};
18 | align-items: flex-start;
19 |
20 | div {
21 | display: flex;
22 | flex-direction: column;
23 | width: 100%;
24 |
25 | label {
26 | font-weight: bold;
27 | color: ${colors.darkGray};
28 | margin-top: 25px;
29 | }
30 |
31 | input {
32 | width: 100%;
33 | height: 44px;
34 | margin: 5px 0 5px;
35 | padding: 0 15px;
36 | border-radius: 4px;
37 | border: 1px solid ${colors.border};
38 | }
39 | }
40 |
41 | div + div {
42 | flex-direction: row;
43 |
44 | label {
45 | width: 100%;
46 | }
47 | label + label {
48 | margin-left: 16px;
49 | }
50 | }
51 | }
52 | `;
53 |
54 | export const Header = styled.div`
55 | display: flex;
56 | justify-content: space-between;
57 | align-items: center;
58 | margin-bottom: 20px;
59 |
60 | h1 {
61 | color: ${colors.darkGray};
62 | }
63 |
64 | div {
65 | display: flex;
66 | align-items: center;
67 |
68 | button {
69 | display: flex;
70 | align-items: center;
71 | padding: 0 10px;
72 | width: 112px;
73 | height: 36px;
74 | border: 0;
75 | border-radius: 4px;
76 | color: ${colors.white};
77 | background: ${colors.backButton};
78 |
79 | &:hover {
80 | background: ${darken(0.04, `${colors.backButton}`)};
81 | }
82 |
83 | span {
84 | padding-left: 10px;
85 | font-weight: bold;
86 | }
87 | }
88 |
89 | button + button {
90 | background: ${colors.primary};
91 | margin-left: 16px;
92 |
93 | &:hover {
94 | background: ${darken(0.04, `${colors.primary}`)};
95 | }
96 | }
97 | }
98 | `;
99 |
--------------------------------------------------------------------------------
/backend/src/app/controllers/OrderController.js:
--------------------------------------------------------------------------------
1 | import { object, string } from 'yup';
2 | import Order from '../models/Order';
3 | import Student from '../models/Student';
4 | import Membership from '../models/Membership';
5 |
6 | class OrderController {
7 | async store(req, res) {
8 | const schema = object().shape({
9 | question: string().required(),
10 | });
11 |
12 | if (!(await schema.isValid(req.body))) {
13 | return res.status(400).json({ error: 'validation fails' });
14 | }
15 |
16 | const { id } = req.params;
17 | const { question } = req.body;
18 |
19 | const checkStudentExists = await Student.findByPk(id);
20 |
21 | if (!checkStudentExists) {
22 | return res.status(401).json({ error: 'Student not found' });
23 | }
24 |
25 | const checkStudentHasMembership = await Membership.findOne({
26 | where: {
27 | student_id: id,
28 | },
29 | });
30 |
31 | if (!checkStudentHasMembership) {
32 | return res
33 | .status(401)
34 | .json({ error: 'Students need a membership to create orders' });
35 | }
36 |
37 | if (!checkStudentHasMembership.active) {
38 | return res
39 | .status(401)
40 | .json({ error: 'Membership must be active to create orders' });
41 | }
42 |
43 | const newOrder = await Order.create({
44 | student_id: id,
45 | question,
46 | });
47 |
48 | const data = {
49 | id: newOrder.id,
50 | question: newOrder.question,
51 | student: {
52 | id: checkStudentExists.id,
53 | name: checkStudentExists.name,
54 | },
55 | };
56 |
57 | const ownerSocket = req.admin;
58 |
59 | if (ownerSocket) {
60 | req.io.to('admin').emit('new_order', data);
61 | }
62 |
63 | return res.json(newOrder);
64 | }
65 |
66 | async index(req, res) {
67 | const { page = 1 } = req.query;
68 |
69 | const { count, rows: orders } = await Order.findAndCountAll({
70 | where: {
71 | student_id: req.params.id,
72 | },
73 | order: [['createdAt', 'DESC']],
74 | limit: 5,
75 | offset: (page - 1) * 5,
76 | });
77 |
78 | return res.json({
79 | orders,
80 | count,
81 | });
82 | }
83 | }
84 |
85 | export default new OrderController();
86 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "challenge-09",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@rocketseat/unform": "1.6.2",
7 | "axios": "0.21.1",
8 | "date-fns": "2.11.0",
9 | "debounce-promise": "3.1.2",
10 | "history": "4.10.1",
11 | "immer": "8.0.1",
12 | "jwt-decode": "2.2.0",
13 | "polished": "3.7.2",
14 | "prop-types": "15.7.2",
15 | "react": "16.14.0",
16 | "react-activity": "1.2.2",
17 | "react-confirm-alert": "2.6.1",
18 | "react-datepicker": "2.14.0",
19 | "react-dom": "16.14.0",
20 | "react-icons": "3.9.0",
21 | "react-input-mask": "2.0.4",
22 | "react-number-format": "4.4.1",
23 | "react-redux": "7.2.0",
24 | "react-router-dom": "5.1.2",
25 | "react-scripts": "3.4.0",
26 | "react-select": "3.0.8",
27 | "react-toastify": "5.5.0",
28 | "reactotron-react-js": "3.3.7",
29 | "reactotron-redux": "3.1.2",
30 | "reactotron-redux-saga": "4.2.3",
31 | "redux": "4.0.5",
32 | "redux-persist": "6.0.0",
33 | "redux-saga": "1.1.3",
34 | "socket.io-client": "2.3.0",
35 | "styled-components": "5.0.1",
36 | "yup": "0.28.3"
37 | },
38 | "scripts": {
39 | "start": "react-app-rewired start",
40 | "build": "react-app-rewired build",
41 | "test": "react-app-rewired test",
42 | "eject": "react-scripts eject"
43 | },
44 | "eslintConfig": {
45 | "extends": "react-app"
46 | },
47 | "browserslist": {
48 | "production": [
49 | ">0.2%",
50 | "not dead",
51 | "not op_mini all"
52 | ],
53 | "development": [
54 | "last 1 chrome version",
55 | "last 1 firefox version",
56 | "last 1 safari version"
57 | ]
58 | },
59 | "devDependencies": {
60 | "babel-eslint": "10.1.0",
61 | "babel-plugin-root-import": "6.4.1",
62 | "customize-cra": "0.9.1",
63 | "eslint": "6.8.0",
64 | "eslint-config-airbnb": "18.1.0",
65 | "eslint-config-prettier": "6.10.0",
66 | "eslint-import-resolver-babel-plugin-root-import": "1.1.1",
67 | "eslint-plugin-import": "2.20.1",
68 | "eslint-plugin-jsx-a11y": "6.2.3",
69 | "eslint-plugin-prettier": "3.1.2",
70 | "eslint-plugin-react": "7.19.0",
71 | "eslint-plugin-react-hooks": "2.5.1",
72 | "prettier": "1.19.1",
73 | "react-app-rewired": "2.1.5"
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/mobile/src/pages/ProfilePage/Profile/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import Icon from 'react-native-vector-icons/MaterialIcons';
4 | import { parseISO, formatDistance } from 'date-fns';
5 | import pt from 'date-fns/locale/pt-BR';
6 |
7 | import Background from '~/components/Background';
8 | import {
9 | Container,
10 | MembershipInfo,
11 | Title,
12 | StudentName,
13 | StudentEmail,
14 | Label,
15 | Name,
16 | Email,
17 | Plan,
18 | PlanTitle,
19 | Membership,
20 | Active,
21 | Until,
22 | LogoutButton,
23 | } from './styles';
24 | import { signOut } from '~/store/modules/student/actions';
25 |
26 | export default function Profile() {
27 | const profile = useSelector(state => state.student.profile);
28 | const membership = useSelector(state => state.student.membership);
29 |
30 | const formattedDate = formatDistance(
31 | parseISO(membership.end_date),
32 | new Date(),
33 | {
34 | addSuffix: true,
35 | locale: pt,
36 | }
37 | );
38 |
39 | const dispatch = useDispatch();
40 |
41 | const handleLogout = () => {
42 | dispatch(signOut());
43 | };
44 |
45 | return (
46 |
47 |
48 |
49 | Perfil
50 |
51 |
52 | {profile.name}
53 |
54 |
55 |
56 | {profile.email}
57 |
58 |
59 |
60 | {membership.active ? (
61 |
62 |
63 | {`expira ${formattedDate}`}
64 |
65 | ) : (
66 |
67 | )}
68 |
69 |
70 |
71 | {membership.plan.title}
72 |
73 |
74 | Logout
75 |
76 |
77 |
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/frontend/src/pages/Students/ManageStudent/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { darken } from 'polished';
3 | import colors from '~/styles/colors';
4 |
5 | export const Container = styled.div`
6 | width: 100%;
7 | max-width: 1200px;
8 | min-width: 550px;
9 | margin: 30px auto;
10 |
11 | form {
12 | width: 100%;
13 | display: flex;
14 | flex-direction: column;
15 | background: ${colors.white};
16 | padding: 5px 30px 30px;
17 | align-items: flex-start;
18 |
19 | div {
20 | display: flex;
21 | flex-direction: column;
22 | width: 100%;
23 |
24 | label {
25 | font-weight: bold;
26 | color: ${colors.darkGray};
27 | margin-top: 25px;
28 | }
29 |
30 | label + label {
31 | }
32 |
33 | input {
34 | width: 100%;
35 | height: 44px;
36 | margin: 5px 0 5px;
37 | padding: 0 15px;
38 | border-radius: 4px;
39 | border: 1px solid ${colors.border};
40 | }
41 | }
42 |
43 | div + div {
44 | flex-direction: row;
45 |
46 | label {
47 | width: 100%;
48 | }
49 | label + label {
50 | margin-left: 16px;
51 | }
52 | }
53 | }
54 | `;
55 |
56 | export const Header = styled.div`
57 | display: flex;
58 | justify-content: space-between;
59 | align-items: center;
60 | margin-bottom: 20px;
61 |
62 | h1 {
63 | color: ${colors.darkGray};
64 | }
65 |
66 | div {
67 | display: flex;
68 | align-items: center;
69 |
70 | button {
71 | display: flex;
72 | align-items: center;
73 | padding: 0 10px;
74 | width: 112px;
75 | height: 36px;
76 | border: 0;
77 | border-radius: 4px;
78 | color: ${colors.white};
79 | background: ${colors.backButton};
80 |
81 | &:hover {
82 | background: ${darken(0.04, `${colors.backButton}`)};
83 | }
84 |
85 | span {
86 | padding-left: 10px;
87 | font-weight: bold;
88 | }
89 | }
90 |
91 | button + button {
92 | background: ${colors.primary};
93 | margin-left: 16px;
94 |
95 | &:hover {
96 | background: ${darken(0.03, `${colors.primary}`)};
97 | }
98 | }
99 | }
100 | `;
101 |
--------------------------------------------------------------------------------
/backend/src/routes.js:
--------------------------------------------------------------------------------
1 | import { Router } from 'express';
2 |
3 | import UserController from './app/controllers/UserController';
4 | import StudentController from './app/controllers/StudentController';
5 | import SessionController from './app/controllers/SessionController';
6 | import PlanController from './app/controllers/PlanController';
7 | import MembershipController from './app/controllers/MembershipController';
8 | import CheckinController from './app/controllers/CheckinController';
9 | import OrderController from './app/controllers/OrderController';
10 | import AnswerController from './app/controllers/AnswerController';
11 |
12 | import authMiddleware from './app/middlewares/auth';
13 |
14 | const routes = new Router();
15 |
16 | routes.post('/users', UserController.store);
17 | routes.post('/sessions', SessionController.store);
18 |
19 | routes.get('/students/:id/checkins', CheckinController.index);
20 | routes.post('/students/:id/checkins', CheckinController.store);
21 |
22 | routes.post('/students/:id/help-orders', OrderController.store);
23 | routes.get('/students/:id/help-orders', OrderController.index);
24 |
25 | /**
26 | * Authentication middleware, all routes that
27 | * need authenticated user comes after this
28 | */
29 | routes.use(authMiddleware);
30 |
31 | routes.put('/users', UserController.update);
32 |
33 | routes.post('/help-orders/:id/answer', AnswerController.store);
34 | routes.get('/help-orders', AnswerController.index);
35 |
36 | routes.post('/students', StudentController.store);
37 | routes.get('/students', StudentController.index);
38 | routes.get('/students/:id', StudentController.show);
39 | routes.put('/students/:id', StudentController.update);
40 | routes.delete('/students/:id', StudentController.delete);
41 |
42 | routes.post('/plans', PlanController.store);
43 | routes.get('/plans', PlanController.index);
44 | routes.get('/plans/:id', PlanController.show);
45 | routes.put('/plans/:id', PlanController.update);
46 | routes.delete('/plans/:id', PlanController.delete);
47 |
48 | routes.post('/memberships', MembershipController.store);
49 | routes.get('/memberships', MembershipController.index);
50 | routes.get('/memberships/:studentId', MembershipController.show);
51 | routes.put('/memberships/:studentId', MembershipController.update);
52 | routes.delete('/memberships/:studentId', MembershipController.delete);
53 |
54 | export default routes;
55 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/frontend/src/assets/logo-header.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/backend/src/app/controllers/UserController.js:
--------------------------------------------------------------------------------
1 | import { object, string, ref } from 'yup';
2 | import User from '../models/User';
3 |
4 | class UserController {
5 | async store(req, res) {
6 | const schema = object().shape({
7 | name: string().required(),
8 | email: string()
9 | .email()
10 | .required(),
11 | password: string()
12 | .required()
13 | .min(6),
14 | });
15 |
16 | if (!(await schema.isValid(req.body))) {
17 | return res.status(400).json({ error: 'validation fails' });
18 | }
19 |
20 | const userExists = await User.findOne({ where: { email: req.body.email } });
21 |
22 | if (userExists)
23 | return res.status(400).json({ error: 'User already exists.' });
24 |
25 | const { id, name, email } = await User.create(req.body);
26 |
27 | return res.json({
28 | id,
29 | name,
30 | email,
31 | });
32 | }
33 |
34 | async update(req, res) {
35 | const schema = object().shape({
36 | name: string(),
37 | email: string().email(),
38 | oldPassword: string().min(6),
39 | password: string()
40 | .min(6)
41 | .when('oldPassword', (oldPassword, field) =>
42 | oldPassword ? field.required() : field
43 | ),
44 | confirmPassword: string().when('password', (password, field) =>
45 | password ? field.required().oneOf([ref('password')]) : field
46 | ),
47 | });
48 |
49 | if (!(await schema.isValid(req.body))) {
50 | return res.status(400).json({ error: 'validation fails' });
51 | }
52 |
53 | const { email, oldPassword, password } = req.body;
54 |
55 | const user = await User.findByPk(req.userId);
56 |
57 | if (email !== user.email) {
58 | const userExists = await User.findOne({ where: { email } });
59 |
60 | if (userExists) {
61 | return res.status(400).json({ error: 'User already exists.' });
62 | }
63 | }
64 |
65 | if (!oldPassword && password) {
66 | return res.status(401).json({ error: 'OldPassword is required.' });
67 | }
68 |
69 | if (oldPassword && !(await user.checkPassword(oldPassword))) {
70 | return res.status(401).json({ error: 'Password does not match' });
71 | }
72 |
73 | const { id, name } = await user.update(req.body);
74 |
75 | return res.json({
76 | id,
77 | name,
78 | email,
79 | });
80 | }
81 | }
82 |
83 | export default new UserController();
84 |
--------------------------------------------------------------------------------
/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.facebook.react.ReactNativeHost;
8 | import com.facebook.react.ReactPackage;
9 | import com.facebook.soloader.SoLoader;
10 | import java.lang.reflect.InvocationTargetException;
11 | import java.util.List;
12 |
13 | public class MainApplication extends Application implements ReactApplication {
14 |
15 | private final ReactNativeHost mReactNativeHost =
16 | new ReactNativeHost(this) {
17 | @Override
18 | public boolean getUseDeveloperSupport() {
19 | return BuildConfig.DEBUG;
20 | }
21 |
22 | @Override
23 | protected List getPackages() {
24 | @SuppressWarnings("UnnecessaryLocalVariable")
25 | List packages = new PackageList(this).getPackages();
26 | // Packages that cannot be autolinked yet can be added manually here, for example:
27 | // packages.add(new MyReactNativePackage());
28 | return packages;
29 | }
30 |
31 | @Override
32 | protected String getJSMainModuleName() {
33 | return "index";
34 | }
35 | };
36 |
37 | @Override
38 | public ReactNativeHost getReactNativeHost() {
39 | return mReactNativeHost;
40 | }
41 |
42 | @Override
43 | public void onCreate() {
44 | super.onCreate();
45 | SoLoader.init(this, /* native exopackage */ false);
46 | initializeFlipper(this); // Remove this line if you don't want Flipper enabled
47 | }
48 |
49 | /**
50 | * Loads Flipper in React Native templates.
51 | *
52 | * @param context
53 | */
54 | private static void initializeFlipper(Context context) {
55 | if (BuildConfig.DEBUG) {
56 | try {
57 | /*
58 | We use reflection here to pick up the class that initializes Flipper,
59 | since Flipper library is not available in release mode
60 | */
61 | Class> aClass = Class.forName("com.facebook.flipper.ReactNativeFlipper");
62 | aClass.getMethod("initializeFlipper", Context.class).invoke(null, context);
63 | } catch (ClassNotFoundException e) {
64 | e.printStackTrace();
65 | } catch (NoSuchMethodException e) {
66 | e.printStackTrace();
67 | } catch (IllegalAccessException e) {
68 | e.printStackTrace();
69 | } catch (InvocationTargetException e) {
70 | e.printStackTrace();
71 | }
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/frontend/src/pages/Memberships/ManageMembership/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { darken } from 'polished';
3 | import colors from '~/styles/colors';
4 |
5 | export const Container = styled.div`
6 | width: 100%;
7 | max-width: 950px;
8 | min-width: 550px;
9 | margin: 30px auto;
10 |
11 | form#form-memberships {
12 | width: 100%;
13 | display: flex;
14 | flex-direction: column;
15 | padding: 5px 30px 30px;
16 | background: ${colors.white};
17 | align-items: flex-start;
18 |
19 | label {
20 | width: 100%;
21 | margin: 25px 0 5px;
22 | }
23 | label + label {
24 | margin-left: 16px;
25 | }
26 | }
27 | `;
28 |
29 | export const Header = styled.div`
30 | display: flex;
31 | justify-content: space-between;
32 | align-items: center;
33 | margin-bottom: 20px;
34 |
35 | h1 {
36 | color: ${colors.darkGray};
37 | }
38 |
39 | div {
40 | display: flex;
41 | align-items: center;
42 |
43 | button {
44 | display: flex;
45 | align-items: center;
46 | padding: 0 10px;
47 | width: 112px;
48 | height: 36px;
49 | border: 0;
50 | border-radius: 4px;
51 | color: ${colors.white};
52 | background: ${colors.backButton};
53 |
54 | &:hover {
55 | background: ${darken(0.04, `${colors.backButton}`)};
56 | }
57 |
58 | span {
59 | padding-left: 10px;
60 | font-weight: bold;
61 | }
62 | }
63 |
64 | button + button {
65 | background: ${colors.primary};
66 | margin-left: 16px;
67 |
68 | &:hover {
69 | background: ${darken(0.03, `${colors.primary}`)};
70 | }
71 | }
72 | }
73 | `;
74 |
75 | export const Student = styled.div`
76 | width: 100%;
77 | padding-top: 25px;
78 |
79 | label {
80 | font-weight: bold;
81 | color: ${colors.darkGray};
82 | }
83 | `;
84 |
85 | export const Info = styled.div`
86 | display: flex;
87 | flex-direction: row;
88 |
89 | label {
90 | font-weight: bold;
91 | color: ${colors.darkGray};
92 | width: 100%;
93 | }
94 |
95 | input[name='start_date'],
96 | input[name='end_date'],
97 | input[name='price'] {
98 | width: 100%;
99 | height: 45px;
100 | margin: 7px 0 0;
101 | padding: 0 15px;
102 | border-radius: 4px;
103 | border: 1px solid ${colors.border};
104 | }
105 |
106 | @media (max-width: 588px) {
107 | width: 100%;
108 | flex-direction: column;
109 |
110 | label {
111 | display: flex;
112 | flex-direction: column;
113 | margin: 20px 0 0 !important;
114 | }
115 | }
116 | `;
117 |
--------------------------------------------------------------------------------
/backend/src/app/controllers/PlanController.js:
--------------------------------------------------------------------------------
1 | import { object, string, number } from 'yup';
2 | import Plan from '../models/Plan';
3 |
4 | class PlanController {
5 | async store(req, res) {
6 | const schema = object().shape({
7 | title: string().required(),
8 | duration: number()
9 | .required()
10 | .integer(),
11 | price: number()
12 | .required()
13 | .positive(),
14 | });
15 |
16 | if (!(await schema.isValid(req.body))) {
17 | return res.status(400).json({ error: 'validation fails' });
18 | }
19 |
20 | const planExists = await Plan.findOne({ where: { title: req.body.title } });
21 |
22 | if (planExists)
23 | return res.status(400).json({ error: 'Plan already exists.' });
24 |
25 | const { id } = await Plan.create(req.body);
26 |
27 | return res.json({
28 | id,
29 | });
30 | }
31 |
32 | async index(req, res) {
33 | const plans = await Plan.findAll();
34 |
35 | return res.json(plans);
36 | }
37 |
38 | async show(req, res) {
39 | const { id } = req.params;
40 |
41 | const plan = await Plan.findByPk(id);
42 |
43 | return res.json(plan);
44 | }
45 |
46 | async update(req, res) {
47 | const schema = object().shape({
48 | title: string().required(),
49 | duration: number()
50 | .integer()
51 | .required(),
52 | price: number()
53 | .positive()
54 | .required(),
55 | });
56 |
57 | if (!(await schema.isValid(req.body))) {
58 | return res.status(400).json({ error: 'validation fails' });
59 | }
60 |
61 | const plan = await Plan.findByPk(req.params.id);
62 |
63 | if (!plan) {
64 | return res.status(401).json({
65 | error: 'Plan does not exist',
66 | });
67 | }
68 |
69 | const { title } = req.body;
70 |
71 | if (title && title !== plan.title) {
72 | const checkTitle = await Plan.findOne({
73 | where: { title },
74 | });
75 |
76 | if (checkTitle) {
77 | return res.status(401).json({
78 | error: 'Plan title must be unique',
79 | });
80 | }
81 | }
82 |
83 | const { duration, price } = await plan.update(req.body);
84 |
85 | return res.json({
86 | title,
87 | duration,
88 | price,
89 | });
90 | }
91 |
92 | async delete(req, res) {
93 | const plan = await Plan.findByPk(req.params.id);
94 |
95 | if (!plan) {
96 | return res.status(400).json({ error: 'Plan not found.' });
97 | }
98 |
99 | await plan.destroy();
100 |
101 | return res.status(204).send();
102 | }
103 | }
104 |
105 | export default new PlanController();
106 |
--------------------------------------------------------------------------------
/backend/__tests__/integration/session.test.js:
--------------------------------------------------------------------------------
1 | import request from 'supertest';
2 | import app from '../../src/app';
3 |
4 | import factory from '../factories';
5 | import truncate from '../util/truncate';
6 | import getToken from '../getToken';
7 |
8 | describe('Authentication', () => {
9 | beforeEach(async () => {
10 | await truncate();
11 | });
12 |
13 | it('should authenticate with valid credentials', async () => {
14 | const response = await getToken();
15 |
16 | expect(response.status).toBe(200);
17 | });
18 |
19 | it('should not authenticate with invalid credentials', async () => {
20 | const user = await factory.create('User', {
21 | password: '123456',
22 | });
23 |
24 | const response = await request(app)
25 | .post('/sessions')
26 | .send({
27 | email: user.email,
28 | password: '123123',
29 | });
30 |
31 | expect(response.status).toBe(401);
32 | });
33 |
34 | it('should return jwt token when authenticated', async () => {
35 | const { body } = await getToken();
36 |
37 | expect(body).toHaveProperty('token');
38 | });
39 |
40 | it('should be able to access private routes when authenticated', async () => {
41 | const { body } = await getToken();
42 |
43 | const { status } = await request(app)
44 | .get('/students')
45 | .set('Authorization', `Bearer ${body.token}`);
46 |
47 | expect(status).toBe(200);
48 | });
49 |
50 | it('should not be able to access private routes without jwt token', async () => {
51 | const response = await request(app).get('/students');
52 |
53 | expect(response.status).toBe(401);
54 | });
55 |
56 | it('should not be able to access private routes with invalid jwt token', async () => {
57 | const response = await request(app)
58 | .get('/students')
59 | .set('Authorization', `Bearer 123456`);
60 |
61 | expect(response.status).toBe(401);
62 | });
63 |
64 | it('should not be able to access when email or password is not provided', async () => {
65 | const user = await factory.create('User', {
66 | password: '123456',
67 | });
68 |
69 | const response = await request(app)
70 | .post('/sessions')
71 | .send({
72 | password: user.password,
73 | });
74 |
75 | expect(response.status).toBe(400);
76 | });
77 |
78 | it('should not be able to access when user does not exist', async () => {
79 | const user = await factory.create('User', {
80 | password: '123456',
81 | });
82 |
83 | const response = await request(app)
84 | .post('/sessions')
85 | .send({
86 | email: 'anotheremail@email.com',
87 | password: user.password,
88 | });
89 |
90 | expect(response.status).toBe(401);
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/frontend/src/pages/Memberships/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { darken } from 'polished';
3 |
4 | import colors from '~/styles/colors';
5 |
6 | export const Container = styled.div`
7 | width: 100%;
8 | max-width: 1380px;
9 | min-width: 550px;
10 | margin: 30px auto;
11 | padding: 0 2px;
12 |
13 | div {
14 | display: flex;
15 | justify-content: center;
16 | flex-direction: column;
17 | align-items: center;
18 | margin-bottom: 10px;
19 |
20 | h1 {
21 | color: ${colors.darkGray};
22 | }
23 |
24 | div {
25 | display: flex;
26 | align-items: center;
27 |
28 | button {
29 | display: flex;
30 | align-items: center;
31 | padding: 0 10px;
32 | width: 142px;
33 | height: 36px;
34 | border: 0;
35 | border-radius: 4px;
36 | color: ${colors.white};
37 | background: ${colors.primary};
38 | margin: 5px 0 10px;
39 |
40 | &:hover {
41 | background: ${darken(0.04, `${colors.primary}`)};
42 | }
43 |
44 | span {
45 | padding-left: 10px;
46 | font-weight: bold;
47 | }
48 | }
49 | }
50 | }
51 |
52 | @media (min-width: 768px) {
53 | div {
54 | flex-direction: row;
55 | justify-content: space-between;
56 | align-items: center;
57 | margin-bottom: 10px;
58 |
59 | div {
60 | button {
61 | margin: 0;
62 | }
63 | }
64 | }
65 | }
66 | `;
67 |
68 | export const MembershipList = styled.ul`
69 | margin-top: 20px;
70 | padding: 30px;
71 | width: 100%;
72 | background: ${colors.white};
73 |
74 | li {
75 | display: grid;
76 | grid-template-columns: 2fr 2fr 2fr 2fr 1fr 1fr;
77 | padding-bottom: 10px;
78 | margin-bottom: 10px;
79 |
80 | strong {
81 | color: ${colors.darkGray};
82 | }
83 |
84 | span {
85 | height: 0;
86 | }
87 |
88 | div {
89 | display: flex;
90 | flex-direction: row;
91 | padding: 0 10px;
92 |
93 | button {
94 | background: none;
95 | border: 0;
96 | color: ${colors.editButton};
97 |
98 | &:hover {
99 | color: ${darken(0.1, `${colors.editButton}`)};
100 | }
101 | }
102 |
103 | button + button {
104 | color: ${colors.primary};
105 | padding-left: 10px;
106 |
107 | &:hover {
108 | color: ${darken(0.1, `${colors.primary}`)};
109 | }
110 | }
111 | }
112 | }
113 |
114 | li + li {
115 | border-bottom: 1px solid ${colors.lightBorder};
116 | }
117 |
118 | li:last-child {
119 | border: 0;
120 | padding-bottom: 0;
121 | margin-bottom: 0;
122 | }
123 | `;
124 |
--------------------------------------------------------------------------------
/frontend/src/pages/Plans/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { darken } from 'polished';
3 |
4 | import colors from '~/styles/colors';
5 |
6 | export const Container = styled.div`
7 | width: 100%;
8 | max-width: 1080px;
9 | min-width: 550px;
10 | margin: 30px auto;
11 | padding: 0 2px;
12 |
13 | div {
14 | display: flex;
15 | justify-content: center;
16 | flex-direction: column;
17 | align-items: center;
18 | margin-bottom: 10px;
19 |
20 | h1 {
21 | color: ${colors.darkGray};
22 | }
23 |
24 | div {
25 | display: flex;
26 | align-items: center;
27 |
28 | button {
29 | display: flex;
30 | align-items: center;
31 | padding: 0 10px;
32 | width: 142px;
33 | height: 36px;
34 | border: 0;
35 | border-radius: 4px;
36 | color: ${colors.white};
37 | background: ${colors.primary};
38 | margin: 5px 0 10px;
39 |
40 | &:hover {
41 | background: ${darken(0.04, `${colors.primary}`)};
42 | }
43 |
44 | span {
45 | padding-left: 10px;
46 | font-weight: bold;
47 | }
48 | }
49 | }
50 | }
51 |
52 | @media (min-width: 768px) {
53 | div {
54 | flex-direction: row;
55 | justify-content: space-between;
56 | align-items: center;
57 | margin-bottom: 10px;
58 |
59 | div {
60 | button {
61 | margin: 0;
62 | }
63 | }
64 | }
65 | }
66 | `;
67 |
68 | export const PlanList = styled.ul`
69 | margin-top: 20px;
70 | padding: 30px;
71 | width: 100%;
72 | background: ${colors.white};
73 |
74 | li {
75 | display: grid;
76 | grid-template-columns: 2fr 1fr 1fr 1fr;
77 | padding-bottom: 10px;
78 | margin-bottom: 10px;
79 |
80 | strong {
81 | color: ${colors.darkGray};
82 | }
83 |
84 | span {
85 | height: 0;
86 | }
87 |
88 | div {
89 | display: flex;
90 | flex-direction: row;
91 | padding: 0 10px;
92 |
93 | button {
94 | background: none;
95 | border: 0;
96 | color: ${colors.editButton};
97 | padding-right: 10px;
98 |
99 | &:hover {
100 | color: ${darken(0.1, `${colors.editButton}`)};
101 | }
102 | }
103 |
104 | button + button {
105 | color: ${colors.primary};
106 |
107 | &:hover {
108 | color: ${darken(0.1, `${colors.primary}`)};
109 | }
110 | }
111 | }
112 | }
113 |
114 | li + li {
115 | border-bottom: 1px solid ${colors.lightBorder};
116 | }
117 |
118 | li:last-child {
119 | border: 0;
120 | padding-bottom: 0;
121 | margin-bottom: 0;
122 | }
123 |
124 | @media (min-width: 768px) {
125 | li {
126 | grid-template-columns: 2fr 2fr 2fr 1fr;
127 | }
128 | `;
129 |
--------------------------------------------------------------------------------
/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 | target 'mobileTests' do
38 | inherit! :search_paths
39 | # Pods for testing
40 | end
41 |
42 | use_native_modules!
43 | end
44 |
45 | target 'mobile-tvOS' do
46 | # Pods for mobile-tvOS
47 |
48 | target 'mobile-tvOSTests' do
49 | inherit! :search_paths
50 | # Pods for testing
51 | end
52 |
53 | end
54 |
--------------------------------------------------------------------------------
/frontend/src/components/Modal/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { darken } from 'polished';
3 |
4 | import colors from '~/styles/colors';
5 |
6 | export const Container = styled.div`
7 | position: fixed;
8 | display: table;
9 | top: 0;
10 | left: 0;
11 | width: 100%;
12 | height: 100%;
13 | background: rgba(0, 0, 0, 0.7);
14 | transform: ${props => (props.visible ? 'scaleY(.01) scaleX(0)' : 'scale(0)')};
15 | animation: ${props =>
16 | props.visible &&
17 | 'unfoldIn 1s cubic-bezier(0.165, 0.840, 0.440, 1.000) forwards'};
18 |
19 | section.modal-main {
20 | position: fixed;
21 | background: white;
22 | width: 450px;
23 | min-height: 350px;
24 | height: auto;
25 | padding: 30px;
26 | left: calc(50% - 225px);
27 | border-radius: 4px;
28 | transform: ${props => props.visible && 'scale(0)'};
29 | animation: ${props =>
30 | props.visible &&
31 | 'zoomIn .5s .8s cubic-bezier(0.165, 0.840, 0.440, 1.000) forwards'};
32 |
33 | strong {
34 | color: ${colors.darkGray};
35 | margin-bottom: 11px;
36 | }
37 |
38 | p {
39 | font-size: 16px;
40 | line-height: 1.8;
41 | color: ${colors.gray};
42 | padding-top: 11px;
43 | }
44 |
45 | div {
46 | display: flex;
47 | flex-direction: column;
48 |
49 | strong.answer {
50 | align-self: flex-start;
51 | margin: 19px 0 5px;
52 | }
53 |
54 | textarea {
55 | width: 100%;
56 | border: 1px solid ${colors.border};
57 | height: 127px;
58 | border-radius: 4px;
59 | padding: 13px 15px;
60 | resize: none;
61 | font-size: 16px;
62 | color: ${colors.gray};
63 | }
64 | }
65 |
66 | button {
67 | display: inline-block;
68 | height: 45px;
69 | width: 100%;
70 | background: ${colors.primary};
71 | color: ${colors.white};
72 | font-weight: bold;
73 | font-size: 16px;
74 | border: 0;
75 | border-radius: 4px;
76 | margin-top: 10px;
77 |
78 | &:hover {
79 | background: ${darken(0.03, `${colors.primary}`)};
80 | }
81 | }
82 | }
83 |
84 | @keyframes unfoldIn {
85 | 0% {
86 | transform: scaleY(0.005) scaleX(0);
87 | }
88 | 50% {
89 | transform: scaleY(0.005) scaleX(1);
90 | }
91 | 100% {
92 | transform: scaleY(1) scaleX(1);
93 | }
94 | }
95 |
96 | @keyframes unfoldOut {
97 | 0% {
98 | transform: scaleY(1) scaleX(1);
99 | }
100 | 50% {
101 | transform: scaleY(0.005) scaleX(1);
102 | }
103 | 100% {
104 | transform: scaleY(0.005) scaleX(0);
105 | }
106 | }
107 |
108 | @keyframes zoomIn {
109 | 0% {
110 | transform: scale(0);
111 | }
112 | 100% {
113 | transform: scale(1);
114 | }
115 | }
116 |
117 | @keyframes zoomOut {
118 | 0% {
119 | transform: scale(1);
120 | }
121 | 100% {
122 | transform: scale(0);
123 | }
124 | }
125 | `;
126 |
--------------------------------------------------------------------------------
/backend/src/app/controllers/CheckinController.js:
--------------------------------------------------------------------------------
1 | import { startOfDay, endOfDay, subDays } from 'date-fns';
2 | import { Op } from 'sequelize';
3 | import Membership from '../models/Membership';
4 | import Student from '../models/Student';
5 | import Plan from '../models/Plan';
6 | import Checkin from '../models/Checkin';
7 |
8 | class CheckinController {
9 | async store(req, res) {
10 | const { id } = req.params;
11 | const checkStudentExists = await Student.findByPk(id);
12 |
13 | if (!checkStudentExists) {
14 | return res.status(401).json({ error: 'Student not found' });
15 | }
16 |
17 | const checkStudentHasMembership = await Membership.findOne({
18 | where: {
19 | student_id: id,
20 | },
21 | });
22 |
23 | if (!checkStudentHasMembership) {
24 | return res
25 | .status(401)
26 | .json({ error: 'Students need a membership to check-in' });
27 | }
28 |
29 | if (!checkStudentHasMembership.active) {
30 | return res
31 | .status(401)
32 | .json({ error: 'Membership must be active to check-in' });
33 | }
34 |
35 | const checkCheckinToday = await Checkin.findOne({
36 | where: {
37 | student_id: id,
38 | createdAt: {
39 | [Op.between]: [startOfDay(new Date()), endOfDay(new Date())],
40 | },
41 | },
42 | });
43 |
44 | if (checkCheckinToday) {
45 | return res.status(401).json({
46 | error: 'You already did your check-in today',
47 | });
48 | }
49 |
50 | const checkins = await Checkin.findAll({
51 | where: {
52 | student_id: id,
53 | created_at: {
54 | [Op.between]: [subDays(new Date(), 7), new Date()],
55 | },
56 | },
57 | });
58 |
59 | if (checkins.length >= 5) {
60 | return res.status(400).json({
61 | error: 'Access denied. You have reached 5 checkins in the last 7 days',
62 | });
63 | }
64 |
65 | const checkin = await Checkin.create({
66 | student_id: id,
67 | });
68 |
69 | return res.json(checkin);
70 | }
71 |
72 | async index(req, res) {
73 | const { page = 1 } = req.query;
74 | const { id } = req.params;
75 | const checkStudentExists = await Student.findByPk(id);
76 |
77 | if (!checkStudentExists) {
78 | return res.status(401).json({ error: 'Student not found' });
79 | }
80 |
81 | const checkStudentHasMembership = await Membership.findOne({
82 | where: {
83 | student_id: id,
84 | },
85 | include: [
86 | {
87 | model: Plan,
88 | as: 'plan',
89 | attributes: ['title'],
90 | },
91 | ],
92 | });
93 |
94 | if (!checkStudentHasMembership) {
95 | return res
96 | .status(401)
97 | .json({ error: 'Students need a membership to check-in' });
98 | }
99 |
100 | const { count, rows: checkins } = await Checkin.findAndCountAll({
101 | where: {
102 | student_id: id,
103 | },
104 | order: [['createdAt', 'DESC']],
105 | limit: 10,
106 | offset: (page - 1) * 10,
107 | });
108 |
109 | return res.json({
110 | checkins,
111 | student: checkStudentExists,
112 | membership: checkStudentHasMembership,
113 | count,
114 | });
115 | }
116 | }
117 |
118 | export default new CheckinController();
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 |
--------------------------------------------------------------------------------
/frontend/src/pages/Students/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { darken } from 'polished';
3 |
4 | import colors from '~/styles/colors';
5 | import search from '~/assets/search.svg';
6 |
7 | export const Container = styled.div`
8 | width: 100%;
9 | max-width: 1200px;
10 | min-width: 550px;
11 | margin: 30px auto;
12 |
13 | div {
14 | display: flex;
15 | justify-content: center;
16 | flex-direction: column;
17 | align-items: center;
18 | margin-bottom: 10px;
19 |
20 | h1 {
21 | color: ${colors.darkGray};
22 | }
23 |
24 | div {
25 | display: flex;
26 | align-items: center;
27 | flex-direction: row;
28 |
29 | button {
30 | display: flex;
31 | align-items: center;
32 | padding: 0 10px;
33 | width: 142px;
34 | height: 36px;
35 | border: 0;
36 | border-radius: 4px;
37 | color: ${colors.white};
38 | background: ${colors.primary};
39 | margin: 5px 0 10px;
40 |
41 | &:hover {
42 | background: ${darken(0.06, `${colors.primary}`)};
43 | }
44 |
45 | span {
46 | padding-left: 10px;
47 | font-weight: bold;
48 | }
49 | }
50 |
51 | input {
52 | margin-left: 10px;
53 | padding-left: 25px;
54 | width: 237px;
55 | height: 36px;
56 | background: #fff url(${search}) no-repeat 5px center;
57 | border-radius: 4px;
58 | border: 1px solid ${colors.border};
59 | }
60 | }
61 | }
62 |
63 | @media (min-width: 768px) {
64 | div {
65 | flex-direction: row;
66 | justify-content: space-between;
67 | align-items: center;
68 | margin-bottom: 10px;
69 |
70 | div {
71 | button {
72 | margin: 0;
73 | }
74 | }
75 | }
76 | }
77 | `;
78 |
79 | export const StudentList = styled.ul`
80 | margin-top: 20px;
81 | padding: 30px;
82 | width: 100%;
83 | background: ${colors.white};
84 |
85 | li {
86 | display: grid;
87 | grid-template-columns: 2fr 2fr 1fr 1fr;
88 | padding-bottom: 10px;
89 | margin-bottom: 10px;
90 |
91 | strong {
92 | color: ${colors.darkGray};
93 | }
94 |
95 | span {
96 | height: 0;
97 | }
98 |
99 | div {
100 | display: flex;
101 | flex-direction: row;
102 | padding: 0 10px;
103 |
104 | button {
105 | background: none;
106 | border: 0;
107 | color: ${colors.editButton};
108 | margin-right: 5px;
109 |
110 | &:hover {
111 | color: ${darken(0.1, `${colors.editButton}`)};
112 | }
113 | }
114 |
115 | button + button {
116 | color: ${colors.primary};
117 |
118 | &:hover {
119 | color: ${darken(0.1, `${colors.primary}`)};
120 | }
121 | }
122 | }
123 | }
124 |
125 | li + li {
126 | border-bottom: 1px solid ${colors.lightBorder};
127 | }
128 |
129 | li:last-child {
130 | border: 0;
131 | padding-bottom: 0;
132 | margin-bottom: 0;
133 | }
134 |
135 | @media (min-width: 768px) {
136 | li {
137 | grid-template-columns: 4fr 3fr 1fr 1fr;
138 | }
139 | }
140 |
141 | @media (min-width: 1000px) {
142 | li {
143 | div {
144 | button {
145 | margin-right: 0;
146 | }
147 | }
148 | }
149 | }
150 | `;
151 |
--------------------------------------------------------------------------------
/backend/src/app/controllers/StudentController.js:
--------------------------------------------------------------------------------
1 | import { object, string, number } from 'yup';
2 | import { Op } from 'sequelize';
3 |
4 | import Student from '../models/Student';
5 |
6 | class StudentController {
7 | async store(req, res) {
8 | const schema = object().shape({
9 | name: string().required(),
10 | email: string()
11 | .email()
12 | .required(),
13 | age: number().required(),
14 | weight: number()
15 | .required()
16 | .positive()
17 | .max(250),
18 | height: number()
19 | .required()
20 | .positive(),
21 | });
22 |
23 | if (!(await schema.isValid(req.body))) {
24 | return res.status(400).json({ error: 'validation fails' });
25 | }
26 |
27 | const studentExists = await Student.findOne({
28 | where: { email: req.body.email },
29 | });
30 |
31 | if (studentExists)
32 | return res.status(400).json({ error: 'Student already exists.' });
33 |
34 | const { id } = await Student.create(req.body);
35 |
36 | return res.json({
37 | id,
38 | });
39 | }
40 |
41 | async index(req, res) {
42 | const { page, filter } = req.query;
43 |
44 | if (filter || page) {
45 | if (!page) {
46 | const students = await Student.findAll({
47 | where: {
48 | name: {
49 | [Op.iLike]: `%${filter}%`,
50 | },
51 | },
52 | order: ['name'],
53 | });
54 |
55 | return res.json(students);
56 | }
57 |
58 | const { count, rows: students } = await Student.findAndCountAll({
59 | where: {
60 | name: {
61 | [Op.iLike]: `%${filter}%`,
62 | },
63 | },
64 | order: ['name'],
65 | limit: 10,
66 | offset: (page - 1) * 10,
67 | });
68 |
69 | return res.json({ students, count });
70 | }
71 |
72 | const students = await Student.findAll({
73 | order: ['name'],
74 | });
75 |
76 | return res.json(students);
77 | }
78 |
79 | async show(req, res) {
80 | const { id } = req.params;
81 |
82 | const student = await Student.findByPk(id);
83 |
84 | return res.json(student);
85 | }
86 |
87 | async update(req, res) {
88 | const { id } = req.params;
89 |
90 | const schema = object().shape({
91 | name: string().required(),
92 | email: string()
93 | .email()
94 | .required(),
95 | age: number()
96 | .required()
97 | .positive()
98 | .max(120)
99 | .integer(),
100 | weight: number()
101 | .required()
102 | .positive()
103 | .max(250),
104 | height: number()
105 | .required()
106 | .positive(),
107 | });
108 |
109 | if (!(await schema.isValid(req.body))) {
110 | return res.status(400).json({ error: 'validation fails' });
111 | }
112 |
113 | const student = await Student.findByPk(id);
114 |
115 | const { name, email, age, weight, height } = await student.update(req.body);
116 |
117 | return res.json({
118 | name,
119 | email,
120 | age,
121 | weight,
122 | height,
123 | });
124 | }
125 |
126 | async delete(req, res) {
127 | const { id } = req.params;
128 |
129 | const student = await Student.findByPk(id);
130 |
131 | if (!student) {
132 | return res.status(400).json({ error: 'Student not found.' });
133 | }
134 |
135 | await student.destroy();
136 |
137 | return res.status(204).send();
138 | }
139 | }
140 |
141 | export default new StudentController();
142 |
--------------------------------------------------------------------------------
/frontend/src/pages/HelpOrders/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useMemo } from 'react';
2 |
3 | import { useSelector } from 'react-redux';
4 | import { toast } from 'react-toastify';
5 | import { FaClipboardCheck } from 'react-icons/fa';
6 | import socketio from 'socket.io-client';
7 |
8 | import Loading from '~/components/Loading';
9 | import Modal from '~/components/Modal';
10 | import api from '~/services/api';
11 |
12 | import { Container, EmptyList, HelpOrdersList } from './styles';
13 |
14 | export default function HelpOrders() {
15 | const [orders, setOrders] = useState([]);
16 | const [order, setOrder] = useState(null);
17 | const [loading, setLoading] = useState(true);
18 |
19 | const user = useSelector(state => state.user.profile);
20 |
21 | const socket = useMemo(
22 | () =>
23 | socketio('http://localhost:3003', {
24 | query: {
25 | user_id: `ADMIN-${user.id}`,
26 | origin: 'web',
27 | },
28 | }),
29 | [user.id]
30 | );
31 |
32 | useEffect(() => {
33 | socket.on('new_order', newOrder => {
34 | console.tron.log(orders, newOrder);
35 | setOrders([...orders, newOrder]);
36 | });
37 | }, [orders, socket]);
38 |
39 | const modalRef = React.createRef();
40 |
41 | const loadOrders = async () => {
42 | const response = await api.get('help-orders');
43 | console.tron.log(response.data);
44 |
45 | setOrders(response.data);
46 | setLoading(false);
47 | };
48 | useEffect(() => {
49 | loadOrders();
50 | }, []);
51 |
52 | const handleAnswer = async answer => {
53 | modalRef.current.setIsComponentVisible(true);
54 | setLoading(true);
55 | try {
56 | await api.post(`/help-orders/${order.id}/answer`, { answer });
57 |
58 | toast.success('Resposta enviada com sucesso');
59 | loadOrders();
60 | } catch (err) {
61 | toast.error(
62 | (err.response && err.response.data.error) ||
63 | 'Erro de comunicação com o servidor'
64 | );
65 | } finally {
66 | setLoading(false);
67 | }
68 | };
69 |
70 | const handleOpen = studentOrder => {
71 | setOrder(studentOrder);
72 | modalRef.current.setIsComponentVisible(true);
73 | };
74 |
75 | return (
76 |
77 | {loading ? (
78 |
79 | ) : (
80 | <>
81 |
82 |
Pedidos de auxílio
83 |
84 | {!orders.length ? (
85 |
86 |
87 | Parabens! Todos os pedidos foram respondidos
88 |
89 | ) : (
90 |
91 |
92 | ALUNO
93 |
94 | {orders.map(o => (
95 |
96 | {o.student.name}
97 |
100 |
101 | ))}
102 |
103 | )}
104 |
105 | {order ? (
106 | <>
107 | PERGUNTA DO ALUNO
108 | {order.question}
109 | >
110 | ) : (
111 | Carregando...
112 | )}
113 |
114 | >
115 | )}
116 |
117 | );
118 | }
119 |
--------------------------------------------------------------------------------
/frontend/src/pages/Plans/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | import { confirmAlert } from 'react-confirm-alert';
4 | import { MdAdd } from 'react-icons/md';
5 | import { toast } from 'react-toastify';
6 |
7 | import Loading from '~/components/Loading';
8 | import history from '~/services/history';
9 | import api from '~/services/api';
10 | import { formatPrice } from '~/helpers/format';
11 |
12 | import { Container, PlanList } from './styles';
13 |
14 | export default function Plans() {
15 | const [plans, setPlans] = useState([]);
16 | const [loading, setLoading] = useState(true);
17 |
18 | const textAlignStyle = {
19 | textAlign: 'center',
20 | };
21 |
22 | useEffect(() => {
23 | const loadPlans = async () => {
24 | const response = await api.get('plans');
25 |
26 | setPlans(response.data);
27 | setLoading(false);
28 | };
29 |
30 | loadPlans();
31 | }, []);
32 |
33 | const handleEdit = id => {
34 | history.push(`/plans/${id}`);
35 | };
36 |
37 | const handleDelete = plan => {
38 | confirmAlert({
39 | title: 'Confirme a exclusão',
40 | message: `Deseja remover o plano ${plan.title} ?`,
41 | buttons: [
42 | {
43 | label: 'Yes',
44 | onClick: async () => {
45 | try {
46 | await api.delete(`plans/${plan.id}`);
47 | toast.success('Plano excluido com sucesso');
48 | setPlans(plans.filter(s => s.id !== plan.id));
49 | } catch (err) {
50 | toast.error(
51 | (err.response && err.response.data.error) ||
52 | 'Erro de comunicação com o servidor'
53 | );
54 | }
55 | },
56 | },
57 | {
58 | label: 'No',
59 | onClick: () => '',
60 | },
61 | ],
62 | });
63 | };
64 |
65 | return (
66 |
67 | {loading ? (
68 |
69 | ) : (
70 | <>
71 |
72 |
Gerenciando planos
73 |
74 |
78 |
79 |
80 | {!plans.length ? (
81 | Nenhum plano encontrado...
82 | ) : (
83 |
84 |
85 | TÍTULO
86 | DURAÇÃO
87 | VALOR p/ MÊS
88 |
89 | {plans.map(plan => (
90 |
91 | {plan.title}
92 | {`${plan.duration} ${
93 | plan.duration === 1 ? 'mês' : 'meses'
94 | }`}
95 | {formatPrice(plan.price)}
96 |
97 |
100 |
103 |
104 |
105 | ))}
106 |
107 | )}
108 | >
109 | )}
110 |
111 | );
112 | }
113 |
--------------------------------------------------------------------------------
/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 Icon from 'react-native-vector-icons/MaterialIcons';
6 |
7 | import SignIn from '~/pages/SignIn';
8 |
9 | import Checkins from '~/pages/CheckInsPage/Checkins';
10 | import HelpOrders from '~/pages/HelpOrdersPages/HelpOrders';
11 | import Answer from '~/pages/HelpOrdersPages/Answer';
12 | import NewOrder from '~/pages/HelpOrdersPages/NewOrder';
13 | import Profile from '~/pages/ProfilePage/Profile';
14 |
15 | import Header from '~/components/Header';
16 |
17 | export default (signedIn = false) =>
18 | createAppContainer(
19 | createSwitchNavigator(
20 | {
21 | Sign: createSwitchNavigator({ SignIn }),
22 | App: createBottomTabNavigator(
23 | {
24 | CheckinsPage: {
25 | screen: createStackNavigator(
26 | {
27 | Checkins,
28 | },
29 | {
30 | headerLayoutPreset: 'center',
31 | defaultNavigationOptions: {
32 | header: ,
33 | headerStyle: {
34 | backgroundColor: '#fff',
35 | },
36 | },
37 | }
38 | ),
39 | navigationOptions: {
40 | tabBarLabel: 'Check-ins',
41 | tabBarIcon: ({ tintColor }) => (
42 |
43 | ),
44 | },
45 | },
46 | HelpOrdersPages: {
47 | screen: createStackNavigator(
48 | { HelpOrders, Answer, NewOrder },
49 | {
50 | defaultNavigationOptions: navigation => ({
51 | header: ,
52 | headerStyle: {
53 | marginTop: 30,
54 | },
55 | }),
56 | }
57 | ),
58 | navigationOptions: {
59 | tabBarLabel: 'Pedir ajuda',
60 | tabBarIcon: ({ tintColor }) => (
61 |
62 | ),
63 | },
64 | },
65 | ProfilePage: {
66 | screen: createStackNavigator(
67 | {
68 | Profile,
69 | },
70 | {
71 | headerLayoutPreset: 'center',
72 | defaultNavigationOptions: {
73 | header: ,
74 | headerStyle: {
75 | backgroundColor: '#fff',
76 | },
77 | },
78 | }
79 | ),
80 | navigationOptions: {
81 | tabBarLabel: 'Profile',
82 | tabBarIcon: ({ tintColor }) => (
83 |
84 | ),
85 | },
86 | },
87 | },
88 | {
89 | resetOnBlur: true,
90 | tabBarOptions: {
91 | keyboardHidesTabBar: true,
92 | activeTintColor: '#ee4e62',
93 | inactiveTintColor: '#999',
94 | },
95 | }
96 | ),
97 | },
98 | {
99 | initialRouteName: signedIn ? 'App' : 'Sign',
100 | }
101 | )
102 | );
103 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/backend/__tests__/integration/answer.test.js:
--------------------------------------------------------------------------------
1 | import request from 'supertest';
2 | import app from '../../src/app';
3 |
4 | import factory from '../factories';
5 | import truncate from '../util/truncate';
6 | import getToken from '../getToken';
7 |
8 | describe('HelpOrders', () => {
9 | beforeEach(async () => {
10 | await truncate();
11 | });
12 |
13 | it('should be able list Help Orders from especific student', async () => {
14 | const student = await factory.attrs('Student');
15 |
16 | const { body } = await getToken();
17 |
18 | const { body: studentID } = await request(app)
19 | .post('/students')
20 | .send(student)
21 | .set('Authorization', `Bearer ${body.token}`);
22 |
23 | const { status } = await request(app).get(
24 | `/students/${studentID.id}/help-orders`
25 | );
26 |
27 | expect(status).toBe(200);
28 | });
29 |
30 | it('should be able to answer a help order', async () => {
31 | const student = await factory.attrs('Student');
32 | const plan = await factory.attrs('Plan');
33 |
34 | const { body } = await getToken();
35 |
36 | const { body: studentID } = await request(app)
37 | .post('/students')
38 | .send(student)
39 | .set('Authorization', `Bearer ${body.token}`);
40 |
41 | const { body: planID } = await request(app)
42 | .post('/plans')
43 | .send(plan)
44 | .set('Authorization', `Bearer ${body.token}`);
45 |
46 | await request(app)
47 | .post('/memberships')
48 | .send({
49 | start_date: new Date(),
50 | plan_id: planID.id,
51 | student_id: studentID.id,
52 | })
53 | .set('Authorization', `Bearer ${body.token}`);
54 |
55 | const { body: order } = await request(app)
56 | .post(`/students/${studentID.id}/help-orders`)
57 | .send({ question: 'Help test - question' });
58 |
59 | const { status } = await request(app)
60 | .post(`/help-orders/${order.id}/answer`)
61 | .send({ answer: 'Help test - answer' })
62 | .set('Authorization', `Bearer ${body.token}`);
63 |
64 | expect(status).toBe(200);
65 | });
66 |
67 | it('should not be able to answer a help order if fail validation', async () => {
68 | const { body } = await getToken();
69 |
70 | await request(app)
71 | .post(`/help-orders/1/answer`)
72 | .send({})
73 | .set('Authorization', `Bearer ${body.token}`);
74 | });
75 |
76 | it('should not be able to answer a help order that was already answered', async () => {
77 | const student = await factory.attrs('Student');
78 | const plan = await factory.attrs('Plan');
79 |
80 | const { body } = await getToken();
81 |
82 | const { body: studentID } = await request(app)
83 | .post('/students')
84 | .send(student)
85 | .set('Authorization', `Bearer ${body.token}`);
86 |
87 | const { body: planID } = await request(app)
88 | .post('/plans')
89 | .send(plan)
90 | .set('Authorization', `Bearer ${body.token}`);
91 |
92 | await request(app)
93 | .post('/memberships')
94 | .send({
95 | start_date: new Date(),
96 | plan_id: planID.id,
97 | student_id: studentID.id,
98 | })
99 | .set('Authorization', `Bearer ${body.token}`);
100 |
101 | const { body: order } = await request(app)
102 | .post(`/students/${studentID.id}/help-orders`)
103 | .send({ question: 'Help test - question' });
104 |
105 | await request(app)
106 | .post(`/help-orders/${order.id}/answer`)
107 | .send({ answer: 'Help test - answer' })
108 | .set('Authorization', `Bearer ${body.token}`);
109 |
110 | const { status } = await request(app)
111 | .post(`/help-orders/${order.id}/answer`)
112 | .send({ answer: 'Help test - answer' })
113 | .set('Authorization', `Bearer ${body.token}`);
114 |
115 | expect(status).toBe(401);
116 | });
117 | });
118 |
--------------------------------------------------------------------------------