├── .gitignore ├── mobile ├── .watchmanconfig ├── .gitattributes ├── src │ ├── components │ │ ├── DateInput │ │ │ ├── index.js │ │ │ ├── styles.js │ │ │ ├── index.android.js │ │ │ └── index.ios.js │ │ ├── Background │ │ │ └── index.js │ │ ├── Button │ │ │ ├── styles.js │ │ │ └── index.js │ │ ├── Input │ │ │ ├── styles.js │ │ │ └── index.js │ │ └── Appointment │ │ │ ├── styles.js │ │ │ └── index.js │ ├── assets │ │ ├── logo.png │ │ ├── logo@2x.png │ │ └── logo@3x.png │ ├── services │ │ └── api.js │ ├── store │ │ ├── modules │ │ │ ├── rootReducer.js │ │ │ ├── rootSaga.js │ │ │ ├── user │ │ │ │ ├── actions.js │ │ │ │ ├── reducer.js │ │ │ │ └── sagas.js │ │ │ └── auth │ │ │ │ ├── actions.js │ │ │ │ ├── reducer.js │ │ │ │ └── sagas.js │ │ ├── createStore.js │ │ ├── persistReducers.js │ │ └── index.js │ ├── App.js │ ├── pages │ │ ├── Dashboard │ │ │ ├── styles.js │ │ │ └── index.js │ │ ├── SelectDateTime │ │ │ ├── styles.js │ │ │ └── index.js │ │ ├── Confirm │ │ │ ├── styles.js │ │ │ └── index.js │ │ ├── SelectProvider │ │ │ ├── styles.js │ │ │ └── index.js │ │ ├── SignIn │ │ │ ├── styles.js │ │ │ └── index.js │ │ ├── SignUp │ │ │ ├── styles.js │ │ │ └── index.js │ │ └── Profile │ │ │ ├── styles.js │ │ │ └── index.js │ ├── config │ │ └── reactotronConfig.js │ ├── index.js │ └── routes.js ├── app.json ├── .prettierrc ├── 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 │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ ├── gradle.properties │ ├── build.gradle │ ├── gradlew.bat │ └── gradlew ├── 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 │ └── mobile.xcodeproj │ │ └── xcshareddata │ │ └── xcschemes │ │ ├── mobile.xcscheme │ │ └── mobile-tvOS.xcscheme ├── jsconfig.json ├── .buckconfig ├── .editorconfig ├── index.js ├── babel.config.js ├── __tests__ │ └── App-test.js ├── metro.config.js ├── .gitignore ├── .eslintrc.js ├── package.json └── .flowconfig ├── api ├── .gitignore ├── src │ ├── server.js │ ├── .prettierrc │ ├── queue.js │ ├── config │ │ ├── auth.js │ │ ├── redis.js │ │ ├── sentry.js │ │ ├── mail.js │ │ ├── database.js │ │ └── multer.js │ ├── app │ │ ├── views │ │ │ └── emails │ │ │ │ ├── partials │ │ │ │ └── footer.hbs │ │ │ │ ├── layouts │ │ │ │ └── default.hbs │ │ │ │ └── cancellation.hbs │ │ ├── controllers │ │ │ ├── FileController.js │ │ │ ├── AvailableController.js │ │ │ ├── ProviderController.js │ │ │ ├── NotificationController.js │ │ │ ├── ScheduleController.js │ │ │ ├── SessionController.js │ │ │ ├── AppointmentController.js │ │ │ └── UserController.js │ │ ├── schemas │ │ │ └── Notification.js │ │ ├── models │ │ │ ├── File.js │ │ │ ├── User.js │ │ │ └── Appointment.js │ │ ├── middlewares │ │ │ └── auth.js │ │ ├── jobs │ │ │ └── CancellationMail.js │ │ └── services │ │ │ ├── AvailableService.js │ │ │ ├── CancelAppointmentService.js │ │ │ └── CreateAppointmentService.js │ ├── database │ │ ├── migrations │ │ │ ├── 20190903164034-add-avatar-field-to-users.js │ │ │ ├── 20190903162550-create-files.js │ │ │ ├── 20190826234338-create-users.js │ │ │ └── 20190904131019-create-appointments.js │ │ └── index.js │ ├── lib │ │ ├── Cache.js │ │ ├── Queue.js │ │ └── Mail.js │ ├── app.js │ └── routes.js ├── nodemon.json ├── tmp │ └── uploads │ │ ├── 22cb3ec8306ab5b82774a35fd2e8739b.jpeg │ │ ├── 30571b2588b0f9b8fa938c9d830c0319.png │ │ ├── 8f8d0ecbd87b9f12a96745300d78f773.jpeg │ │ ├── 94873740e3d7e1d7cdfc9937b4e6445b.jpg │ │ ├── 9aa9abd50f246653cbc88bdb18b2af63.jpeg │ │ └── de4d5380cad6f81eace20285e036b9dd.jpeg ├── .editorconfig ├── .sequelizerc ├── .env.example ├── .eslintrc.js ├── docker-compose.yml ├── package.json └── README.MD ├── web ├── .prettierrc ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── assets │ │ ├── print1.png │ │ ├── print2.png │ │ ├── print3.png │ │ ├── print4.png │ │ ├── print5.png │ │ ├── print6.png │ │ ├── logo.svg │ │ └── headerLogo.svg │ ├── services │ │ ├── history.js │ │ └── api.js │ ├── index.js │ ├── pages │ │ ├── _layouts │ │ │ ├── default │ │ │ │ ├── styles.js │ │ │ │ └── index.js │ │ │ └── auth │ │ │ │ ├── index.js │ │ │ │ └── styles.js │ │ ├── Profile │ │ │ ├── AvatarInput │ │ │ │ ├── styles.js │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── Dashboard │ │ │ ├── styles.js │ │ │ └── index.js │ │ ├── SignIn │ │ │ └── index.js │ │ └── SignUp │ │ │ └── index.js │ ├── store │ │ ├── modules │ │ │ ├── rootReducer.js │ │ │ ├── rootSaga.js │ │ │ ├── user │ │ │ │ ├── actions.js │ │ │ │ ├── reducer.js │ │ │ │ └── sagas.js │ │ │ └── auth │ │ │ │ ├── actions.js │ │ │ │ ├── reducer.js │ │ │ │ └── sagas.js │ │ ├── persistReducers.js │ │ ├── createStore.js │ │ └── index.js │ ├── config │ │ └── ReactotronConfig.js │ ├── routes │ │ ├── index.js │ │ └── Route.js │ ├── App.js │ ├── styles │ │ └── global.js │ └── components │ │ ├── header │ │ ├── index.js │ │ └── styles.js │ │ └── Notifications │ │ ├── styles.js │ │ └── index.js ├── jsconfig.json ├── .editorconfig ├── config-overrides.js ├── .gitignore ├── readme.md ├── .eslintrc.js └── package.json ├── zImages ├── print1.png ├── print2.png ├── print3.png ├── print4.png ├── print5.png ├── print6.png ├── backend.png ├── mobile1.png └── mobile2.png └── README.MD /.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mobile/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /mobile/.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /mobile/src/components/DateInput/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | data 4 | -------------------------------------------------------------------------------- /api/src/server.js: -------------------------------------------------------------------------------- 1 | import app from './app'; 2 | 3 | app.listen(3333); 4 | -------------------------------------------------------------------------------- /mobile/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobile", 3 | "displayName": "mobile" 4 | } -------------------------------------------------------------------------------- /web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /api/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "execMap": { 3 | "js": "sucrase-node" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /api/src/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /mobile/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /zImages/print1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/zImages/print1.png -------------------------------------------------------------------------------- /zImages/print2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/zImages/print2.png -------------------------------------------------------------------------------- /zImages/print3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/zImages/print3.png -------------------------------------------------------------------------------- /zImages/print4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/zImages/print4.png -------------------------------------------------------------------------------- /zImages/print5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/zImages/print5.png -------------------------------------------------------------------------------- /zImages/print6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/zImages/print6.png -------------------------------------------------------------------------------- /zImages/backend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/zImages/backend.png -------------------------------------------------------------------------------- /zImages/mobile1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/zImages/mobile1.png -------------------------------------------------------------------------------- /zImages/mobile2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/zImages/mobile2.png -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/web/public/favicon.ico -------------------------------------------------------------------------------- /web/src/assets/print1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/web/src/assets/print1.png -------------------------------------------------------------------------------- /web/src/assets/print2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/web/src/assets/print2.png -------------------------------------------------------------------------------- /web/src/assets/print3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/web/src/assets/print3.png -------------------------------------------------------------------------------- /web/src/assets/print4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/web/src/assets/print4.png -------------------------------------------------------------------------------- /web/src/assets/print5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/web/src/assets/print5.png -------------------------------------------------------------------------------- /web/src/assets/print6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/web/src/assets/print6.png -------------------------------------------------------------------------------- /mobile/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/mobile/src/assets/logo.png -------------------------------------------------------------------------------- /api/src/queue.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | import Queue from './lib/Queue'; 4 | 5 | Queue.processQueue(); 6 | -------------------------------------------------------------------------------- /mobile/src/assets/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/mobile/src/assets/logo@2x.png -------------------------------------------------------------------------------- /mobile/src/assets/logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/mobile/src/assets/logo@3x.png -------------------------------------------------------------------------------- /api/src/config/auth.js: -------------------------------------------------------------------------------- 1 | export default { 2 | secret: '1b8f6eb9f24c06d411c7ed5154dd08fe', 3 | expiresIn: '7d', 4 | }; 5 | -------------------------------------------------------------------------------- /api/src/config/redis.js: -------------------------------------------------------------------------------- 1 | export default { 2 | host: process.env.REDIS_HOST, 3 | port: process.env.REDIS_PORT, 4 | }; 5 | -------------------------------------------------------------------------------- /mobile/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/mobile/android/app/debug.keystore -------------------------------------------------------------------------------- /api/src/config/sentry.js: -------------------------------------------------------------------------------- 1 | export default { 2 | dsn: 'https://c52bbfaa965d45298f4e9eb1f98fec9b@sentry.io/1713822', 3 | }; 4 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | mobile 3 | 4 | -------------------------------------------------------------------------------- /api/src/app/views/emails/partials/footer.hbs: -------------------------------------------------------------------------------- 1 |
2 | Equipe GoBarber 3 | -------------------------------------------------------------------------------- /mobile/ios/mobile/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /mobile/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions":{ 3 | "baseUrl":"src", 4 | "paths":{ 5 | "~/*":["*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /web/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "paths": { 5 | "~/*": ["*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /mobile/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/mobile/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /mobile/.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /api/tmp/uploads/22cb3ec8306ab5b82774a35fd2e8739b.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/api/tmp/uploads/22cb3ec8306ab5b82774a35fd2e8739b.jpeg -------------------------------------------------------------------------------- /api/tmp/uploads/30571b2588b0f9b8fa938c9d830c0319.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/api/tmp/uploads/30571b2588b0f9b8fa938c9d830c0319.png -------------------------------------------------------------------------------- /api/tmp/uploads/8f8d0ecbd87b9f12a96745300d78f773.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/api/tmp/uploads/8f8d0ecbd87b9f12a96745300d78f773.jpeg -------------------------------------------------------------------------------- /api/tmp/uploads/94873740e3d7e1d7cdfc9937b4e6445b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/api/tmp/uploads/94873740e3d7e1d7cdfc9937b4e6445b.jpg -------------------------------------------------------------------------------- /api/tmp/uploads/9aa9abd50f246653cbc88bdb18b2af63.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/api/tmp/uploads/9aa9abd50f246653cbc88bdb18b2af63.jpeg -------------------------------------------------------------------------------- /api/tmp/uploads/de4d5380cad6f81eace20285e036b9dd.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/api/tmp/uploads/de4d5380cad6f81eace20285e036b9dd.jpeg -------------------------------------------------------------------------------- /web/src/services/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history'; 2 | 3 | const history = createBrowserHistory(); 4 | 5 | export default history; 6 | -------------------------------------------------------------------------------- /web/src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const api = axios.create({ 4 | baseURL: 'http://localhost:3333', 5 | }); 6 | 7 | export default api; 8 | -------------------------------------------------------------------------------- /mobile/src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const api = axios.create({ 4 | baseURL: 'http://192.168.0.12:3333', 5 | }); 6 | 7 | export default api; 8 | -------------------------------------------------------------------------------- /api/.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 -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/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/jonathanwdev/GoBarber/HEAD/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/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/jonathanwdev/GoBarber/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/jonathanwdev/GoBarber/HEAD/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /web/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 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/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/jonathanwdev/GoBarber/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/jonathanwdev/GoBarber/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/jonathanwdev/GoBarber/HEAD/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanwdev/GoBarber/HEAD/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /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 10 | -------------------------------------------------------------------------------- /web/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /api/src/app/views/emails/layouts/default.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{{ body }}} 3 | {{> footer }} 4 |
5 | -------------------------------------------------------------------------------- /web/src/pages/_layouts/default/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Wrapper = styled.div` 4 | height: 100%; 5 | background: linear-gradient(-90deg, #7159c1, #ab59c1); 6 | `; 7 | -------------------------------------------------------------------------------- /web/src/store/modules/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import auth from './auth/reducer'; 3 | import user from './user/reducer'; 4 | 5 | export default combineReducers({ auth, user }); 6 | -------------------------------------------------------------------------------- /mobile/src/store/modules/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import auth from './auth/reducer'; 3 | import user from './user/reducer'; 4 | 5 | export default combineReducers({ auth, user }); 6 | -------------------------------------------------------------------------------- /mobile/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import { AppRegistry } from 'react-native'; 6 | import App from './src'; 7 | import { name as appName } from './app.json'; 8 | 9 | AppRegistry.registerComponent(appName, () => App); 10 | -------------------------------------------------------------------------------- /mobile/src/store/modules/rootSaga.js: -------------------------------------------------------------------------------- 1 | import { all } from 'redux-saga/effects'; 2 | import auth from './auth/sagas'; 3 | import user from './user/sagas'; 4 | 5 | export default function* rootSaga() { 6 | return yield all([auth, user]); 7 | } 8 | -------------------------------------------------------------------------------- /web/src/store/modules/rootSaga.js: -------------------------------------------------------------------------------- 1 | import { all } from 'redux-saga/effects'; 2 | import auth from './auth/sagas'; 3 | import user from './user/sagas'; 4 | 5 | export default function* rootSaga() { 6 | return yield all([auth, user]); 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /web/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-5.5-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /mobile/src/components/Background/index.js: -------------------------------------------------------------------------------- 1 | import LinearGradient from 'react-native-linear-gradient'; 2 | import styled from 'styled-components/native'; 3 | 4 | export default styled(LinearGradient).attrs({ 5 | colors: ['#7159c1', '#ab59c1'], 6 | })` 7 | flex: 1; 8 | `; 9 | -------------------------------------------------------------------------------- /api/src/config/mail.js: -------------------------------------------------------------------------------- 1 | export default { 2 | host: process.env.MAIL_HOST, 3 | port: process.env.MAIL_PORT, 4 | secure: false, 5 | auth: { 6 | user: process.env.MAIL_USER, 7 | pass: process.env.MAIL_PASS, 8 | }, 9 | default: { 10 | from: 'Equipe GOBarber ', 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /mobile/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | import CreateRouter from './routes'; 5 | 6 | export default function App() { 7 | const signed = useSelector(state => state.auth.signed); 8 | const Routes = CreateRouter(signed); 9 | 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /api/.sequelizerc: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | module.exports = { 3 | config: resolve(__dirname, "src", "config", "database.js"), 4 | "models-path": resolve(__dirname, "src", "app", "models"), 5 | "migrations-path": resolve(__dirname, "src", "database", "migrations"), 6 | "seeders-path": resolve(__dirname, "src", "database", "migrations") 7 | }; 8 | -------------------------------------------------------------------------------- /mobile/src/store/createStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } 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 | -------------------------------------------------------------------------------- /api/src/app/views/emails/cancellation.hbs: -------------------------------------------------------------------------------- 1 | Olá, {{ provider }}
2 |

Houve um cancelamento de horario, confira os detalhes abaixo:

3 |

4 | Cliente: {{ user }}
5 | Data/Hora: {{ date }}

6 | 7 | O horario está disponivel para novos agendamentos!! 8 | 9 | 10 |

11 | -------------------------------------------------------------------------------- /api/src/app/controllers/FileController.js: -------------------------------------------------------------------------------- 1 | import File from '../models/File'; 2 | 3 | class FileController { 4 | async store(req, res) { 5 | const { originalname: name, filename: path } = req.file; 6 | const file = await File.create({ 7 | name, 8 | path, 9 | }); 10 | return res.json(file); 11 | } 12 | } 13 | 14 | export default new FileController(); 15 | -------------------------------------------------------------------------------- /mobile/metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /api/src/config/database.js: -------------------------------------------------------------------------------- 1 | require('dotenv/config'); 2 | 3 | module.exports = { 4 | dialect: 'postgres', 5 | host: process.env.POSTGRES_HOST, 6 | username: process.env.POSTGRES_USER, 7 | password: process.env.POSTGRES_PASSWORD, 8 | database: process.env.POSTGRES_DB, 9 | define: { 10 | timestamps: true, 11 | underscored: true, 12 | underscoredAll: true, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /web/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: 'gobarber', 8 | storage, 9 | whitelist: ['auth', 'user'], 10 | }, 11 | reducers 12 | ); 13 | 14 | return persistedReducer; 15 | }; 16 | -------------------------------------------------------------------------------- /mobile/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'mobile' 2 | include ':react-native-linear-gradient' 3 | project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android') 4 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 5 | include ':app' 6 | -------------------------------------------------------------------------------- /web/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 | -------------------------------------------------------------------------------- /api/.env.example: -------------------------------------------------------------------------------- 1 | APP_URL=http://localhost:3333 2 | NODE_ENV=development 3 | 4 | 5 | # auth 6 | APP_SECRET= 7 | 8 | # database 9 | POSTGRES_HOST= 10 | POSTGRES_USER= 11 | POSTGRES_PASSWORD= 12 | POSTGRES_DB= 13 | 14 | # mongo 15 | MONGO_URL= 16 | 17 | # Redis 18 | REDIS_HOST= 19 | REDIS_PORT= 20 | 21 | # MAIL 22 | MAIL_HOST= 23 | MAIL_PORT= 24 | MAIL_USER= 25 | MAIL_PASS= 26 | 27 | # SENTRY 28 | 29 | SENTRY_DSN= 30 | -------------------------------------------------------------------------------- /web/.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 | -------------------------------------------------------------------------------- /web/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 | tron.clear(); 11 | console.tron = tron; 12 | } 13 | -------------------------------------------------------------------------------- /mobile/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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: 'gobarber', 8 | storage: AsyncStorage, 9 | whitelist: ['auth', 'user'], 10 | }, 11 | reducers 12 | ); 13 | 14 | return persistedReducer; 15 | }; 16 | -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React App 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /web/src/store/createStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } from 'redux'; 2 | 3 | export default (reducers, middlewares) => { 4 | const enhancer = 5 | process.env.NODE_ENV === 'development' 6 | ? compose( 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: 46px; 6 | background: #3b9eff; 7 | border-radius: 4px; 8 | 9 | align-items: center; 10 | justify-content: center; 11 | `; 12 | 13 | export const Text = styled.Text` 14 | color: #fff; 15 | font-weight: bold; 16 | font-size: 16px; 17 | `; 18 | -------------------------------------------------------------------------------- /web/src/pages/_layouts/default/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Header from '~/components/header'; 4 | 5 | import { Wrapper } from './styles'; 6 | 7 | export default function DefaultLayout({ children }) { 8 | return ( 9 | 10 |
11 | {children} 12 | 13 | ); 14 | } 15 | 16 | DefaultLayout.propTypes = { 17 | children: PropTypes.element.isRequired, 18 | }; 19 | -------------------------------------------------------------------------------- /mobile/ios/mobile/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (nonatomic, strong) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /web/src/store/modules/user/actions.js: -------------------------------------------------------------------------------- 1 | export function updateProfileRequest(data) { 2 | return { 3 | type: '@user/UPDATE_PROFILE_REQUEST', 4 | payload: { data }, 5 | }; 6 | } 7 | 8 | export function updateProfileSuccess(profile) { 9 | return { 10 | type: '@user/UPDATE_PROFILE_SUCCESS', 11 | payload: { profile }, 12 | }; 13 | } 14 | 15 | export function updateProfileFailure() { 16 | return { 17 | type: '@user/UPDATE_PROFILE_FAILURE', 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /mobile/src/pages/Dashboard/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Container = styled.SafeAreaView` 4 | flex: 1; 5 | `; 6 | 7 | export const Title = styled.Text` 8 | font-size: 20px; 9 | color: #fff; 10 | font-weight: bold; 11 | align-self: center; 12 | margin-top: 30px; 13 | `; 14 | 15 | export const List = styled.FlatList.attrs({ 16 | showsVerticalScrollIndicator: false, 17 | contentContainerStyle: { padding: 30 }, 18 | })``; 19 | -------------------------------------------------------------------------------- /mobile/src/store/modules/user/actions.js: -------------------------------------------------------------------------------- 1 | export function updateProfileRequest(data) { 2 | return { 3 | type: '@user/UPDATE_PROFILE_REQUEST', 4 | payload: { data }, 5 | }; 6 | } 7 | 8 | export function updateProfileSuccess(profile) { 9 | return { 10 | type: '@user/UPDATE_PROFILE_SUCCESS', 11 | payload: { profile }, 12 | }; 13 | } 14 | 15 | export function updateProfileFailure() { 16 | return { 17 | type: '@user/UPDATE_PROFILE_FAILURE', 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /api/src/config/multer.js: -------------------------------------------------------------------------------- 1 | import multer from 'multer'; 2 | import crypto from 'crypto'; 3 | import { extname, resolve } from 'path'; 4 | 5 | export default { 6 | storage: multer.diskStorage({ 7 | destination: resolve(__dirname, '..', '..', 'tmp', 'uploads'), 8 | filename: (req, file, cb) => { 9 | crypto.randomBytes(16, (err, res) => { 10 | if (err) return cb(err); 11 | 12 | return cb(null, res.toString('hex') + extname(file.originalname)); 13 | }); 14 | }, 15 | }), 16 | }; 17 | -------------------------------------------------------------------------------- /api/src/database/migrations/20190903164034-add-avatar-field-to-users.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.addColumn('users', 'avatar_id', { 4 | type: Sequelize.INTEGER, 5 | references: { model: 'files', key: 'id' }, 6 | onUpdate: 'CASCADE', 7 | onDelete: 'SET NULL', 8 | allowNull: true, 9 | }); 10 | }, 11 | 12 | down: queryInterface => { 13 | return queryInterface.removeColumn('users', 'avatar_id'); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /mobile/src/components/Input/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Container = styled.View` 4 | padding: 0 15px; 5 | height: 46px; 6 | background: rgba(0, 0, 0, 0.1); 7 | border-radius: 4px; 8 | 9 | flex-direction: row; 10 | align-items: center; 11 | `; 12 | 13 | export const TInput = styled.TextInput.attrs({ 14 | placeholderTextColor: 'rgba(255, 255, 255, 0.8)', 15 | })` 16 | flex: 1; 17 | font-size: 15px; 18 | margin-left: 10px; 19 | color: #fff; 20 | `; 21 | -------------------------------------------------------------------------------- /api/src/app/schemas/Notification.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const NotificationSchema = new mongoose.Schema( 4 | { 5 | content: { 6 | type: String, 7 | required: true, 8 | }, 9 | user: { 10 | type: Number, 11 | required: true, 12 | }, 13 | read: { 14 | type: Boolean, 15 | required: true, 16 | default: false, 17 | }, 18 | }, 19 | { 20 | timestamps: true, 21 | } 22 | ); 23 | 24 | export default mongoose.model('Notification', NotificationSchema); 25 | -------------------------------------------------------------------------------- /web/src/pages/Profile/AvatarInput/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | align-self: center; 5 | margin-bottom: 30px; 6 | 7 | label { 8 | cursor: pointer; 9 | 10 | &:hover { 11 | opacity: 0.7; 12 | } 13 | 14 | img { 15 | height: 120px; 16 | width: 120px; 17 | border-radius: 50%; 18 | border: 3px solid rgba(255, 255, 255, 0.3); 19 | background: #eee; 20 | } 21 | 22 | input { 23 | display: none; 24 | } 25 | } 26 | `; 27 | -------------------------------------------------------------------------------- /api/src/app/models/File.js: -------------------------------------------------------------------------------- 1 | import Sequelize, { Model } from 'sequelize'; 2 | 3 | class File extends Model { 4 | static init(sequelize) { 5 | super.init( 6 | { 7 | name: Sequelize.STRING, 8 | path: Sequelize.STRING, 9 | url: { 10 | type: Sequelize.VIRTUAL, 11 | get() { 12 | return `${process.env.APP_URL}/files/${this.path}`; 13 | }, 14 | }, 15 | }, 16 | { 17 | sequelize, 18 | } 19 | ); 20 | 21 | return this; 22 | } 23 | } 24 | 25 | export default File; 26 | -------------------------------------------------------------------------------- /mobile/src/config/reactotronConfig.js: -------------------------------------------------------------------------------- 1 | import Reactotron from 'reactotron-react-native'; 2 | import { reactotronRedux } from 'reactotron-redux'; 3 | import reactotronSaga from 'reactotron-redux-saga'; 4 | import AsyncStorage from '@react-native-community/async-storage'; 5 | 6 | if (__DEV__) { 7 | const tron = Reactotron.setAsyncStorageHandler(AsyncStorage) 8 | .configure({ host: '192.168.0.12' }) 9 | .useReactNative() 10 | .use(reactotronRedux()) 11 | .use(reactotronSaga()) 12 | .connect(); 13 | 14 | tron.clear(); 15 | 16 | console.tron = tron; 17 | } 18 | -------------------------------------------------------------------------------- /api/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true 5 | }, 6 | extends: ["airbnb-base", "prettier"], 7 | plugins: ["prettier"], 8 | globals: { 9 | Atomics: "readonly", 10 | SharedArrayBuffer: "readonly" 11 | }, 12 | parserOptions: { 13 | ecmaVersion: 2018, 14 | sourceType: "module" 15 | }, 16 | rules: { 17 | "prettier/prettier": "error", 18 | "class-methods-use-this": "off", 19 | "no-param-reassign": "off", 20 | camelcase: "off", 21 | "no-unused-vars": ["error", { argsIgnorePattern: "next" }] 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /api/src/app/controllers/AvailableController.js: -------------------------------------------------------------------------------- 1 | import AvailableService from '../services/AvailableService'; 2 | 3 | class AvailableController { 4 | async index(req, res) { 5 | const { date } = req.query; 6 | if (!date) { 7 | res.status(400).json({ error: 'Invalid date' }); 8 | } 9 | 10 | const searchDate = Number(date); 11 | 12 | const available = await AvailableService.run({ 13 | date: searchDate, 14 | provider_id: req.params.providerId, 15 | }); 16 | 17 | res.json(available); 18 | } 19 | } 20 | 21 | export default new AvailableController(); 22 | -------------------------------------------------------------------------------- /mobile/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PersistGate } from 'redux-persist/integration/react'; 3 | import { Provider } from 'react-redux'; 4 | import { StatusBar } from 'react-native'; 5 | 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 | } 20 | -------------------------------------------------------------------------------- /mobile/src/components/DateInput/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Container = styled.View` 4 | margin: 60px 0 30px; 5 | `; 6 | export const DateButton = styled.TouchableOpacity` 7 | padding: 0 15px; 8 | height: 46px; 9 | background: rgba(0, 0, 0, 0.1); 10 | border-radius: 4px; 11 | margin: 0 30px; 12 | flex-direction: row; 13 | align-items: center; 14 | `; 15 | export const DateText = styled.Text` 16 | color: #fff; 17 | font-size: 14px; 18 | margin-left: 15px; 19 | `; 20 | 21 | export const Picker = styled.Text` 22 | background: #fff; 23 | padding: 15px 30px; 24 | margin-top: 30px; 25 | `; 26 | -------------------------------------------------------------------------------- /api/src/app/middlewares/auth.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import { promisify } from 'util'; 3 | import authConfig from '../../config/auth'; 4 | 5 | export default async (req, res, next) => { 6 | const authHeader = req.headers.authorization; 7 | if (!authHeader) { 8 | return res.status(401).json({ error: 'token not provided' }); 9 | } 10 | 11 | const [, token] = authHeader.split(' '); 12 | 13 | try { 14 | const decoded = await promisify(jwt.verify)(token, authConfig.secret); 15 | req.userId = decoded.id; 16 | 17 | return next(); 18 | } catch (err) { 19 | return res.status(401).json({ error: 'token invalid' }); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /web/src/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch } from 'react-router-dom'; 3 | import Route from './Route'; 4 | import Signin from '../pages/SignIn'; 5 | import Signup from '../pages/SignUp'; 6 | 7 | import Dashboard from '../pages/Dashboard'; 8 | import Profile from '../pages/Profile'; 9 | 10 | export default function Routes() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /mobile/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 | return produce(state, draft => { 9 | switch (action.type) { 10 | case '@auth/SIGN_IN_SUCCESS': { 11 | draft.profile = action.payload.user; 12 | break; 13 | } 14 | 15 | case '@user/UPDATE_PROFILE_SUCCESS': { 16 | draft.profile = action.payload.profile; 17 | break; 18 | } 19 | 20 | case '@auth/SIGN_OUT': { 21 | draft.profile = null; 22 | break; 23 | } 24 | 25 | default: 26 | } 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /web/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 | return produce(state, draft => { 9 | switch (action.type) { 10 | case '@auth/SIGN_IN_SUCCESS': { 11 | draft.profile = action.payload.user; 12 | break; 13 | } 14 | 15 | case '@user/UPDATE_PROFILE_SUCCESS': { 16 | draft.profile = action.payload.profile; 17 | break; 18 | } 19 | 20 | case '@auth/SIGN_OUT': { 21 | draft.profile = null; 22 | break; 23 | } 24 | 25 | default: 26 | } 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /mobile/src/components/Button/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActivityIndicator } from 'react-native'; 3 | 4 | import PropTypes from 'prop-types'; 5 | 6 | import { Container, Text } from './styles'; 7 | 8 | export default function Button({ children, loading, ...rest }) { 9 | return ( 10 | 11 | {loading ? ( 12 | 13 | ) : ( 14 | {children} 15 | )} 16 | 17 | ); 18 | } 19 | Button.propTypes = { 20 | children: PropTypes.string.isRequired, 21 | loading: PropTypes.bool, 22 | }; 23 | 24 | Button.defaultProps = { 25 | loading: false, 26 | }; 27 | -------------------------------------------------------------------------------- /mobile/android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /mobile/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { persistStore } from 'redux-persist'; 2 | import createSagaMiddleware from 'redux-saga'; 3 | 4 | import createStore from './createStore'; 5 | import persistReducer from './persistReducers'; 6 | 7 | import rootReducer from './modules/rootReducer'; 8 | import rootSaga from './modules/rootSaga'; 9 | 10 | const sagaMonitor = __DEV__ ? console.tron.createSagaMonitor() : null; 11 | const sagaMiddleware = createSagaMiddleware({ sagaMonitor }); 12 | 13 | const middlewares = [sagaMiddleware]; 14 | 15 | const store = createStore(persistReducer(rootReducer), middlewares); 16 | const persistor = persistStore(store); 17 | 18 | sagaMiddleware.run(rootSaga); 19 | 20 | export { store, persistor }; 21 | -------------------------------------------------------------------------------- /mobile/src/pages/SelectDateTime/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | import { RectButton } from 'react-native-gesture-handler'; 3 | 4 | export const Container = styled.SafeAreaView` 5 | flex: 1; 6 | `; 7 | export const HourList = styled.FlatList.attrs({ 8 | numColumns: 2, 9 | showsVerticalScrollIndicator: false, 10 | })` 11 | padding: 0 20px; 12 | `; 13 | export const Hour = styled(RectButton)` 14 | background: #fff; 15 | border-radius: 4px; 16 | padding: 20px; 17 | flex: 1; 18 | opacity: ${props => (props.enabled ? 1 : 0.6)}; 19 | align-items: center; 20 | margin: 0 10px 20px; 21 | `; 22 | export const Title = styled.Text` 23 | font-size: 14px; 24 | font-weight: bold; 25 | color: #333; 26 | `; 27 | -------------------------------------------------------------------------------- /mobile/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 signUpRequest(name, email, password) { 16 | return { 17 | type: '@auth/SIGN_UP_REQUEST', 18 | payload: { name, email, password }, 19 | }; 20 | } 21 | 22 | export function signFailure() { 23 | return { 24 | type: '@auth/SIGN_FAILURE', 25 | }; 26 | } 27 | 28 | export function signOut() { 29 | return { 30 | type: '@auth/SIGN_OUT', 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /web/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 signUpRequest(name, email, password) { 16 | return { 17 | type: '@auth/SIGN_UP_REQUEST', 18 | payload: { name, email, password }, 19 | }; 20 | } 21 | 22 | export function signFailure() { 23 | return { 24 | type: '@auth/SIGN_FAILURE', 25 | }; 26 | } 27 | 28 | export function signOut() { 29 | return { 30 | type: '@auth/SIGN_OUT', 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /mobile/src/components/Input/index.js: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Icon from 'react-native-vector-icons/MaterialIcons'; 4 | 5 | import { Container, TInput } from './styles'; 6 | 7 | function Input({ style, icon, ...rest }, ref) { 8 | return ( 9 | 10 | {Icon && } 11 | 12 | 13 | ); 14 | } 15 | 16 | Input.propTypes = { 17 | icon: PropTypes.string, 18 | style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), 19 | }; 20 | 21 | Input.defaultProps = { 22 | icon: null, 23 | style: {}, 24 | }; 25 | 26 | export default forwardRef(Input); 27 | -------------------------------------------------------------------------------- /web/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { persistStore } from 'redux-persist'; 2 | import createSagaMiddleware from 'redux-saga'; 3 | 4 | import createStore from './createStore'; 5 | import persistReducer from './persistReducers'; 6 | 7 | import rootReducer from './modules/rootReducer'; 8 | import rootSaga from './modules/rootSaga'; 9 | 10 | const sagaMonitor = 11 | process.env.NODE_ENV === 'development' 12 | ? console.tron.createSagaMonitor() 13 | : null; 14 | const sagaMiddleware = createSagaMiddleware({ sagaMonitor }); 15 | 16 | const middlewares = [sagaMiddleware]; 17 | 18 | const store = createStore(persistReducer(rootReducer), middlewares); 19 | const persistor = persistStore(store); 20 | 21 | sagaMiddleware.run(rootSaga); 22 | 23 | export { store, persistor }; 24 | -------------------------------------------------------------------------------- /mobile/src/pages/Confirm/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | import Button from '~/components/Button'; 3 | 4 | export const Container = styled.SafeAreaView` 5 | flex: 1; 6 | padding: 0 30px; 7 | justify-content: center; 8 | align-items: center; 9 | `; 10 | export const Avatar = styled.Image` 11 | width: 120px; 12 | height: 120px; 13 | border-radius: 60px; 14 | `; 15 | export const Name = styled.Text` 16 | margin-top: 10px; 17 | font-weight: 20px; 18 | font-weight: bold; 19 | color: #fff; 20 | `; 21 | export const Time = styled.Text` 22 | margin-top: 4px; 23 | font-size: 18px; 24 | color: rgba(255, 255, 255, 0.6); 25 | `; 26 | export const SubmitButton = styled(Button)` 27 | align-self: stretch; 28 | margin-top: 20px; 29 | `; 30 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /web/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ToastContainer } from 'react-toastify'; 3 | import { PersistGate } from 'redux-persist/es/integration/react'; 4 | import { Provider } from 'react-redux'; 5 | import { Router } from 'react-router-dom'; 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 GobalStyle 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 | -------------------------------------------------------------------------------- /api/src/app/controllers/ProviderController.js: -------------------------------------------------------------------------------- 1 | import User from '../models/User'; 2 | import File from '../models/File'; 3 | 4 | import Cache from '../../lib/Cache'; 5 | 6 | class ProviderController { 7 | async index(req, res) { 8 | const cached = await Cache.get('providers'); 9 | 10 | if (cached) { 11 | return res.json(cached); 12 | } 13 | 14 | const providers = await User.findAll({ 15 | where: { provider: true }, 16 | attributes: ['id', 'name', 'email', 'avatar_id'], 17 | include: [ 18 | { 19 | model: File, 20 | as: 'avatar', 21 | attributes: { exclude: ['id', 'createdAt', 'updatedAt'] }, 22 | }, 23 | ], 24 | }); 25 | 26 | await Cache.set('providers', providers); 27 | 28 | return res.json(providers); 29 | } 30 | } 31 | 32 | export default new ProviderController(); 33 | -------------------------------------------------------------------------------- /api/src/database/migrations/20190903162550-create-files.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.createTable('files', { 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 | path: { 15 | type: Sequelize.STRING, 16 | allowNull: false, 17 | unique: true, 18 | }, 19 | created_at: { 20 | type: Sequelize.DATE, 21 | allowNull: false, 22 | }, 23 | updated_at: { 24 | type: Sequelize.DATE, 25 | allowNull: false, 26 | }, 27 | }); 28 | }, 29 | 30 | down: queryInterface => { 31 | return queryInterface.dropTable('files'); 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /api/src/lib/Cache.js: -------------------------------------------------------------------------------- 1 | import Redis from 'ioredis'; 2 | 3 | class Cache { 4 | constructor() { 5 | this.redis = new Redis({ 6 | host: process.env.REDIS_HOST, 7 | port: process.env.REDIS_PORT, 8 | keyPrefix: 'cache:', 9 | }); 10 | } 11 | 12 | set(key, value) { 13 | return this.redis.set(key, JSON.stringify(value), 'EX', 60 * 60 * 24); 14 | } 15 | 16 | async get(key) { 17 | const cached = await this.redis.get(key); 18 | 19 | return cached ? JSON.parse(cached) : null; 20 | } 21 | 22 | invalidate(key) { 23 | return this.redis.del(key); 24 | } 25 | 26 | async invalidatePrefix(prefix) { 27 | const keys = await this.redis.keys(`cache:${prefix}:*`); 28 | const keysWithoutPrefix = keys.map(key => key.replace('cache:', '')); 29 | 30 | return this.redis.del(keysWithoutPrefix); 31 | } 32 | } 33 | 34 | export default new Cache(); 35 | -------------------------------------------------------------------------------- /web/src/styles/global.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | import 'react-perfect-scrollbar/dist/css/styles.css'; 3 | import 'react-toastify/dist/ReactToastify.css'; 4 | 5 | export default createGlobalStyle` 6 | @import url('https://fonts.googleapis.com/css?family=Roboto:400,700&display=swap'); 7 | *{ 8 | margin:0; 9 | padding:0; 10 | outline: 0; 11 | box-sizing: border-box; 12 | } 13 | *:focus{ 14 | outline:0; 15 | } 16 | html, body, #root{ 17 | height:100%; 18 | background: linear-gradient(-90deg, #7159c1, #ab59c1); 19 | } 20 | body{ 21 | -webkit-font-smoothing: antialiased; 22 | } 23 | body, input, button{ 24 | font: 14px 'Roboto', sans-serif; 25 | } 26 | a{ 27 | text-decoration: none; 28 | } 29 | li{ 30 | list-style: none; 31 | } 32 | button{ 33 | cursor: pointer; 34 | } 35 | 36 | `; 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /mobile/src/pages/SelectProvider/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | import { RectButton } from 'react-native-gesture-handler'; 3 | 4 | export const Container = styled.SafeAreaView` 5 | flex: 1; 6 | `; 7 | export const ProvidersList = styled.FlatList.attrs({ 8 | showsVerticalScrollIndicator: false, 9 | numColumns: 2, 10 | })` 11 | margin-top: 60px; 12 | padding: 0 20px; 13 | `; 14 | export const Provider = styled(RectButton)` 15 | background: #fff; 16 | border-radius: 4px; 17 | padding: 20px; 18 | flex: 1; 19 | 20 | align-items: center; 21 | margin: 0 10px 20px; 22 | `; 23 | export const Avatar = styled.Image` 24 | width: 50px; 25 | height: 50px; 26 | border-radius: 20px; 27 | `; 28 | export const Name = styled.Text` 29 | margin-top: 15px; 30 | font-size: 14px; 31 | font-weight: bold; 32 | text-align: center; 33 | color: #333; 34 | `; 35 | -------------------------------------------------------------------------------- /web/src/store/modules/auth/reducer.js: -------------------------------------------------------------------------------- 1 | import produce from 'immer'; 2 | 3 | const INITIAL_STATE = { 4 | token: null, 5 | signed: false, 6 | loading: false, 7 | }; 8 | 9 | export default function auth(state = INITIAL_STATE, action) { 10 | return produce(state, draft => { 11 | switch (action.type) { 12 | case '@auth/SIGN_IN_REQUEST': { 13 | draft.loading = true; 14 | break; 15 | } 16 | case '@auth/SIGN_IN_SUCCESS': { 17 | draft.token = action.payload.token; 18 | draft.signed = true; 19 | draft.loading = false; 20 | break; 21 | } 22 | case '@auth/SIGN_FAILURE': { 23 | draft.loading = false; 24 | break; 25 | } 26 | case '@auth/SIGN_OUT': { 27 | draft.token = null; 28 | draft.signed = false; 29 | break; 30 | } 31 | default: 32 | } 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /mobile/src/store/modules/auth/reducer.js: -------------------------------------------------------------------------------- 1 | import produce from 'immer'; 2 | 3 | const INITIAL_STATE = { 4 | token: null, 5 | signed: false, 6 | loading: false, 7 | }; 8 | 9 | export default function auth(state = INITIAL_STATE, action) { 10 | return produce(state, draft => { 11 | switch (action.type) { 12 | case '@auth/SIGN_IN_REQUEST': { 13 | draft.loading = true; 14 | break; 15 | } 16 | case '@auth/SIGN_IN_SUCCESS': { 17 | draft.token = action.payload.token; 18 | draft.signed = true; 19 | draft.loading = false; 20 | break; 21 | } 22 | case '@auth/SIGN_FAILURE': { 23 | draft.loading = false; 24 | break; 25 | } 26 | case '@auth/SIGN_OUT': { 27 | draft.token = null; 28 | draft.signed = false; 29 | break; 30 | } 31 | default: 32 | } 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /api/src/app/jobs/CancellationMail.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 CancellationMail { 6 | get key() { 7 | return 'CancellationMail'; 8 | } 9 | 10 | async handle({ data }) { 11 | const { appointment } = data; 12 | 13 | await Mail.sendMail({ 14 | to: `${appointment.provider.name} <${appointment.provider.email}>`, 15 | subject: ' Agendamento cancelado', 16 | template: 'cancellation', 17 | context: { 18 | provider: appointment.provider.name, 19 | user: appointment.user.name, 20 | date: format( 21 | parseISO(appointment.date), 22 | "'dia' dd 'de' MMMM', ás' H:mm'h'", 23 | { 24 | locale: pt, 25 | } 26 | ), 27 | }, 28 | }); 29 | } 30 | } 31 | 32 | export default new CancellationMail(); 33 | -------------------------------------------------------------------------------- /api/src/database/index.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | import mongoose from 'mongoose'; 3 | import User from '../app/models/User'; 4 | import File from '../app/models/File'; 5 | import Appointment from '../app/models/Appointment'; 6 | 7 | import databaseConfig from '../config/database'; 8 | 9 | const models = [User, File, Appointment]; 10 | 11 | class Database { 12 | constructor() { 13 | this.init(); 14 | this.mongo(); 15 | } 16 | 17 | init() { 18 | this.connection = new Sequelize(databaseConfig); 19 | models 20 | .map(model => model.init(this.connection)) 21 | .map(model => model.associate && model.associate(this.connection.models)); 22 | } 23 | 24 | mongo() { 25 | this.mongoConnection = mongoose.connect(process.env.MONGO_URL, { 26 | useNewUrlParser: true, 27 | useFindAndModify: true, 28 | }); 29 | } 30 | } 31 | 32 | export default new Database(); 33 | -------------------------------------------------------------------------------- /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: 50px; 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 | 30 | export const SignLink = styled.TouchableOpacity` 31 | margin-top: 20px; 32 | `; 33 | 34 | export const SignLinkText = styled.Text` 35 | color: #fff; 36 | font-weight: bold; 37 | font-size: 16px; 38 | `; 39 | -------------------------------------------------------------------------------- /mobile/src/pages/SignUp/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: 50px; 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 | 30 | export const SignLink = styled.TouchableOpacity` 31 | margin-top: 20px; 32 | `; 33 | 34 | export const SignLinkText = styled.Text` 35 | color: #fff; 36 | font-weight: bold; 37 | font-size: 16px; 38 | `; 39 | -------------------------------------------------------------------------------- /api/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 | provider: Sequelize.BOOLEAN, 13 | }, 14 | { 15 | sequelize, 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 | return this; 24 | } 25 | 26 | static associate(models) { 27 | this.belongsTo(models.File, { foreignKey: 'avatar_id', as: 'avatar' }); 28 | } 29 | 30 | checkPassword(password) { 31 | return bcrypt.compare(password, this.password_hash); 32 | } 33 | } 34 | 35 | export default User; 36 | -------------------------------------------------------------------------------- /api/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | volumes: 3 | data: 4 | services: 5 | postgres: 6 | image: postgres:11.1-alpine 7 | restart: always 8 | container_name: gobarber-postgres 9 | working_dir: /application 10 | volumes: 11 | - ./data:/var/lib/postgresql/data 12 | environment: 13 | - POSTGRES_USER=root 14 | - POSTGRES_PASSWORD=root 15 | - POSTGRES_DB=gobarber 16 | ports: 17 | - '5432:5432' 18 | mongo: 19 | image: mongo 20 | restart: always 21 | ports: 22 | - '27017:27017' 23 | # environment: 24 | # MONGO_INITDB_ROOT_USERNAME: root 25 | # MONGO_INITDB_ROOT_PASSWORD: example 26 | redis: 27 | image: redis:alpine 28 | ports: 29 | - '6379:6379' 30 | 31 | # docker run --name database -e POSTGRES_PASSWORD=docker -p 5432:5432 -d postgres 32 | # docker run --name redisbarber -p 6379:6379 -d -t redis:alpine 33 | # docker run --name mongobarber -p 27017:27017 -d -t mongo 34 | -------------------------------------------------------------------------------- /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 | @Override 19 | protected ReactActivityDelegate createReactActivityDelegate() { 20 | return new ReactActivityDelegate(this, getMainComponentName()) { 21 | @Override 22 | protected ReactRootView createRootView() { 23 | return new RNGestureHandlerEnabledRootView(MainActivity.this); 24 | } 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /mobile/src/components/Appointment/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | 3 | export const Container = styled.View` 4 | margin-bottom: 15px; 5 | padding: 20px; 6 | border-radius: 4px; 7 | background: #fff; 8 | 9 | display: flex; 10 | flex-direction: row; 11 | align-items: center; 12 | justify-content: space-between; 13 | 14 | opacity: ${props => (props.past ? 0.6 : 1)}; 15 | `; 16 | 17 | export const Left = styled.View` 18 | display: flex; 19 | flex-direction: row; 20 | align-items: center; 21 | `; 22 | 23 | export const Avatar = styled.Image` 24 | width: 50px; 25 | height: 50px; 26 | border-radius: 25px; 27 | `; 28 | 29 | export const Info = styled.View` 30 | margin-left: 15px; 31 | `; 32 | 33 | export const Name = styled.Text` 34 | font-weight: bold; 35 | font-size: 14px; 36 | color: #333; 37 | `; 38 | 39 | export const Time = styled.Text` 40 | color: #999; 41 | font-size: 13px; 42 | margin-top: 4px; 43 | `; 44 | -------------------------------------------------------------------------------- /web/readme.md: -------------------------------------------------------------------------------- 1 |

GoBarber Web

2 |

The goBarber Web is the provider's version, here he can recieve notifications, he can manage his schedule and check who made an appointment and when has an appointment.

3 |

4 | 5 | 6 | 7 | 8 | 9 | 10 |

11 |

Tecnologies

12 |
    13 |
  • ReactJS: To build a Single page application
  • 14 |
  • Redux - Redux-Saga: for application state management.
  • 15 |
  • RocketSeat/unform: Work with forms and components
  • 16 |
  • React Toastify: show alerts
  • 17 |
  • Styled Components: to work with css in a different way
  • 18 |
  • Polished: to work with colors
  • 19 |
  • React Icons: to use icons packages
  • 20 |
21 | -------------------------------------------------------------------------------- /api/src/app/controllers/NotificationController.js: -------------------------------------------------------------------------------- 1 | import User from '../models/User'; 2 | import Notification from '../schemas/Notification'; 3 | 4 | class NotificationController { 5 | async index(req, res) { 6 | const CheckisProvider = await User.findOne({ 7 | where: { id: req.userId, provider: true }, 8 | }); 9 | 10 | if (!CheckisProvider) { 11 | return res 12 | .status(401) 13 | .json({ error: 'Only providers can load notifications' }); 14 | } 15 | const notifications = await Notification.find({ 16 | user: req.userId, 17 | }) 18 | .sort({ createdAt: 'desc' }) 19 | .limit(20); 20 | 21 | return res.json(notifications); 22 | } 23 | 24 | async update(req, res) { 25 | const notify = await Notification.findByIdAndUpdate( 26 | req.params.id, 27 | { read: true }, 28 | { new: true } 29 | ); 30 | 31 | return res.json(notify); 32 | } 33 | } 34 | 35 | export default new NotificationController(); 36 | -------------------------------------------------------------------------------- /api/src/lib/Queue.js: -------------------------------------------------------------------------------- 1 | import Bee from 'bee-queue'; 2 | import CancellationMail from '../app/jobs/CancellationMail'; 3 | import redisConfig from '../config/redis'; 4 | 5 | const jobs = [CancellationMail]; 6 | 7 | class Queue { 8 | constructor() { 9 | this.queues = {}; 10 | 11 | this.init(); 12 | } 13 | 14 | init() { 15 | jobs.forEach(({ key, handle }) => { 16 | this.queues[key] = { 17 | bee: new Bee(key, { 18 | redis: redisConfig, 19 | }), 20 | handle, 21 | }; 22 | }); 23 | } 24 | 25 | add(queue, job) { 26 | return this.queues[queue].bee.createJob(job).save(); 27 | } 28 | 29 | processQueue() { 30 | jobs.forEach(job => { 31 | const { bee, handle } = this.queues[job.key]; 32 | 33 | bee.on('failed', this.handleFailure).process(handle); 34 | }); 35 | } 36 | 37 | handleFailure(job, err) { 38 | console.log(`Queue ${job.queue.name}: FAILED`, err); 39 | } 40 | } 41 | 42 | export default new Queue(); 43 | -------------------------------------------------------------------------------- /mobile/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | android.useAndroidX=true 21 | android.enableJetifier=true 22 | -------------------------------------------------------------------------------- /mobile/src/pages/Profile/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native'; 2 | import Input from '~/components/Input'; 3 | import Button from '~/components/Button'; 4 | 5 | export const Container = styled.SafeAreaView` 6 | flex: 1; 7 | `; 8 | 9 | export const Title = styled.Text` 10 | font-size: 20px; 11 | color: #fff; 12 | font-weight: bold; 13 | align-self: center; 14 | margin-top: 30px; 15 | `; 16 | 17 | export const Separator = styled.View` 18 | height: 1px; 19 | background: rgba(255, 255, 255, 0.2); 20 | margin: 20px 0 30px; 21 | `; 22 | 23 | export const Form = styled.ScrollView.attrs({ 24 | showsVerticalIndicator: false, 25 | contentContainerStyle: { padding: 30 }, 26 | })` 27 | align-self: stretch; 28 | `; 29 | 30 | export const FormInput = styled(Input)` 31 | margin-bottom: 10px; 32 | `; 33 | 34 | export const SubmitButton = styled(Button)` 35 | margin-top: 5px; 36 | `; 37 | 38 | export const LogOutButton = styled(Button)` 39 | margin-top: 5px; 40 | background: #f64c75; 41 | `; 42 | -------------------------------------------------------------------------------- /api/src/app/models/Appointment.js: -------------------------------------------------------------------------------- 1 | import Sequelize, { Model } from 'sequelize'; 2 | import { isBefore, subHours } from 'date-fns'; 3 | 4 | class Appointment extends Model { 5 | static init(sequelize) { 6 | super.init( 7 | { 8 | date: Sequelize.DATE, 9 | canceled_at: Sequelize.DATE, 10 | past: { 11 | type: Sequelize.VIRTUAL, 12 | get() { 13 | return isBefore(this.date, new Date()); 14 | }, 15 | }, 16 | cancelable: { 17 | type: Sequelize.VIRTUAL, 18 | get() { 19 | return isBefore(new Date(), subHours(this.date, 2)); 20 | }, 21 | }, 22 | }, 23 | { 24 | sequelize, 25 | } 26 | ); 27 | 28 | return this; 29 | } 30 | 31 | static associate(models) { 32 | this.belongsTo(models.User, { foreignKey: 'user_id', as: 'user' }); 33 | this.belongsTo(models.User, { foreignKey: 'provider_id', as: 'provider' }); 34 | } 35 | } 36 | 37 | export default Appointment; 38 | -------------------------------------------------------------------------------- /web/src/store/modules/user/sagas.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-object-spread */ 2 | import { all, takeLatest, call, put } from 'redux-saga/effects'; 3 | import { toast } from 'react-toastify'; 4 | 5 | import api from '~/services/api'; 6 | 7 | import { updateProfileSuccess, updateProfileFailure } from './actions'; 8 | 9 | export function* updateProfile({ payload }) { 10 | try { 11 | // eslint-disable-next-line camelcase 12 | const { name, email, avatar_id, ...rest } = payload.data; 13 | 14 | const profile = Object.assign( 15 | { name, email, avatar_id }, 16 | 17 | rest.oldPassword ? rest : {} 18 | ); 19 | 20 | const response = yield call(api.put, 'users', profile); 21 | 22 | toast.success('Perfil atualizado com sucesso'); 23 | 24 | yield put(updateProfileSuccess(response.data)); 25 | } catch (err) { 26 | toast.error('Erro ao atualizar perfil, confira seus dados!'); 27 | yield put(updateProfileFailure()); 28 | } 29 | } 30 | 31 | export default all([takeLatest('@user/UPDATE_PROFILE_REQUEST', updateProfile)]); 32 | -------------------------------------------------------------------------------- /web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | }, 6 | extends: ['airbnb', 'prettier', 'prettier/react'], 7 | globals: { 8 | Atomics: 'readonly', 9 | SharedArrayBuffer: 'readonly', 10 | }, 11 | parser: 'babel-eslint', 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | ecmaVersion: 2018, 17 | sourceType: 'module', 18 | }, 19 | plugins: ['react', 'prettier', 'react-hooks'], 20 | rules: { 21 | 'prettier/prettier': 'error', 22 | 'react/jsx-filename-extension': ['warn', { extensions: ['.js', '.jsx'] }], 23 | 'import/prefer-default-export': 'off', 24 | 'no-param-reassign': 'off', 25 | 'no-console': ['error', { allow: ['tron'] }], 26 | 'react/jsx-props-no-spreading': 'off', 27 | 'react-hooks/rules-of-hooks': 'error', 28 | 'react-hooks/exhaustive-deps': 'warn', 29 | }, 30 | settings: { 31 | 'import/resolver': { 32 | 'babel-plugin-root-import': { 33 | rootPathSuffix: 'src', 34 | }, 35 | }, 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /mobile/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # Android/IntelliJ 25 | # 26 | build/ 27 | .idea 28 | .gradle 29 | local.properties 30 | *.iml 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | yarn-error.log 37 | 38 | # BUCK 39 | buck-out/ 40 | \.buckd/ 41 | *.keystore 42 | !debug.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # CocoaPods 59 | /ios/Pods/ 60 | -------------------------------------------------------------------------------- /mobile/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /mobile/src/store/modules/user/sagas.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-object-spread */ 2 | import { all, takeLatest, call, put } from 'redux-saga/effects'; 3 | import { Alert } from 'react-native'; 4 | 5 | import api from '~/services/api'; 6 | 7 | import { updateProfileSuccess, updateProfileFailure } from './actions'; 8 | 9 | export function* updateProfile({ payload }) { 10 | try { 11 | // eslint-disable-next-line camelcase 12 | const { name, email, ...rest } = payload.data; 13 | 14 | const profile = Object.assign( 15 | { name, email }, 16 | 17 | rest.oldPassword ? rest : {} 18 | ); 19 | 20 | const response = yield call(api.put, 'users', profile); 21 | 22 | Alert.alert('Sucesso', 'Perfil atualizado com sucesso !!'); 23 | 24 | yield put(updateProfileSuccess(response.data)); 25 | } catch (err) { 26 | Alert.alert( 27 | 'Falha na atualização', 28 | 'Houve um erro na atualização, verifique seus dados!' 29 | ); 30 | yield put(updateProfileFailure()); 31 | } 32 | } 33 | 34 | export default all([takeLatest('@user/UPDATE_PROFILE_REQUEST', updateProfile)]); 35 | -------------------------------------------------------------------------------- /api/src/database/migrations/20190826234338-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 | provider: { 24 | type: Sequelize.BOOLEAN, 25 | defaultValue: false, 26 | allowNull: false, 27 | }, 28 | created_at: { 29 | type: Sequelize.DATE, 30 | allowNull: false, 31 | }, 32 | updated_at: { 33 | type: Sequelize.DATE, 34 | allowNull: false, 35 | }, 36 | }); 37 | }, 38 | 39 | down: queryInterface => { 40 | return queryInterface.dropTable('users'); 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /web/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 | import { store } from '~/store'; 8 | 9 | export default function RouteWrapper({ 10 | component: Component, 11 | isPrivate, 12 | ...rest 13 | }) { 14 | const { signed } = store.getState().auth; 15 | if (!signed && isPrivate) { 16 | return ; 17 | } 18 | if (signed && !isPrivate) { 19 | return ; 20 | } 21 | 22 | const Layout = signed ? DefaultLayout : AuthLayout; 23 | 24 | return ( 25 | ( 28 | 29 | 30 | 31 | )} 32 | /> 33 | ); 34 | } 35 | 36 | RouteWrapper.propTypes = { 37 | isPrivate: PropTypes.bool, 38 | component: PropTypes.oneOfType([PropTypes.element, PropTypes.func]) 39 | .isRequired, 40 | }; 41 | 42 | RouteWrapper.defaultProps = { 43 | isPrivate: false, 44 | }; 45 | -------------------------------------------------------------------------------- /mobile/src/components/DateInput/index.android.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { DatePickerAndroid } from 'react-native'; 3 | import { format } from 'date-fns'; 4 | import pt from 'date-fns/locale/pt'; 5 | 6 | import Icon from 'react-native-vector-icons/MaterialIcons'; 7 | 8 | import { Container, DateButton, DateText } from './styles'; 9 | 10 | export default function DateInput({ date, onChange }) { 11 | const dateFormated = useMemo( 12 | () => format(date, "dd 'de' MMMM 'de' yyyy", { locale: pt }), 13 | [date] 14 | ); 15 | async function handleOpenPicker() { 16 | const { action, year, month, day } = await DatePickerAndroid.open({ 17 | mode: 'spinner', 18 | date, 19 | }); 20 | 21 | if (action === DatePickerAndroid.dateSetAction) { 22 | const selectedDate = new Date(year, month, day); 23 | onChange(selectedDate); 24 | } 25 | } 26 | return ( 27 | 28 | 29 | 30 | {dateFormated} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /web/src/pages/Dashboard/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | max-width: 600px; 5 | margin: 50px auto; 6 | display: flex; 7 | flex-direction: column; 8 | 9 | header { 10 | display: flex; 11 | align-self: center; 12 | align-items: center; 13 | 14 | button { 15 | border: 0; 16 | background: none; 17 | } 18 | strong { 19 | color: #fff; 20 | font-size: 24px; 21 | margin: 0 15px; 22 | } 23 | } 24 | 25 | ul { 26 | display: grid; 27 | grid-template-columns: repeat(2, 1fr); 28 | grid-gap: 15px; 29 | margin-top: 30px; 30 | } 31 | `; 32 | 33 | export const Time = styled.li` 34 | padding: 20px; 35 | border-radius: 4px; 36 | background: #fff; 37 | 38 | opacity: ${props => (props.past ? 0.6 : 1)}; 39 | 40 | strong { 41 | display: block; 42 | color: ${props => (props.available ? '#999' : '#7159c1')}; 43 | font-size: 20px; 44 | font-weight: normal; 45 | } 46 | 47 | span { 48 | display: block; 49 | margin-top: 3px; 50 | color: ${props => (props.available ? '#999' : '#666')}; 51 | } 52 | `; 53 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /api/src/app/controllers/ScheduleController.js: -------------------------------------------------------------------------------- 1 | import { startOfDay, endOfDay, parseISO } from 'date-fns'; 2 | import { Op } from 'sequelize'; 3 | import User from '../models/User'; 4 | import Appointment from '../models/Appointment'; 5 | 6 | class ScheduleController { 7 | async index(req, res) { 8 | const checkUserProvider = await User.findOne({ 9 | where: { id: req.userId, provider: true }, 10 | }); 11 | 12 | if (!checkUserProvider) { 13 | return res.status(401).json({ error: 'User is not a provider' }); 14 | } 15 | 16 | const { date } = req.query; 17 | const parsedDate = parseISO(date); 18 | 19 | const appointments = await Appointment.findAll({ 20 | where: { 21 | provider_id: req.userId, 22 | canceled_at: null, 23 | date: { 24 | [Op.between]: [startOfDay(parsedDate), endOfDay(parsedDate)], 25 | }, 26 | }, 27 | include: [ 28 | { 29 | model: User, 30 | as: 'user', 31 | attributes: ['name'], 32 | }, 33 | ], 34 | order: ['date'], 35 | }); 36 | 37 | return res.json(appointments); 38 | } 39 | } 40 | 41 | export default new ScheduleController(); 42 | -------------------------------------------------------------------------------- /mobile/src/components/DateInput/index.ios.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo } from 'react'; 2 | import { DatePickerIOS } from 'react-native'; 3 | import { format } from 'date-fns'; 4 | import pt from 'date-fns/locale/pt'; 5 | 6 | import Icon from 'react-native-vector-icons/MaterialIcons'; 7 | 8 | import { Container, DateButton, DateText, Picker } from './styles'; 9 | 10 | export default function DateInput({ date, onChange }) { 11 | const [opened, setOpened] = useState(false); 12 | 13 | const dateFormated = useMemo( 14 | () => format(date, "dd 'de' MMMM 'de' yyyy", { locale: pt }), 15 | [date] 16 | ); 17 | 18 | return ( 19 | 20 | setOpened(!opened)}> 21 | 22 | {dateFormated} 23 | 24 | {opened && ( 25 | 26 | 34 | 35 | )} 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /web/src/components/header/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import Notifications from '../Notifications'; 5 | 6 | import logo from '~/assets/headerLogo.svg'; 7 | import { Container, Profile, Content } from './styles'; 8 | 9 | export default function Header() { 10 | const profile = useSelector(state => state.user.profile); 11 | 12 | return ( 13 | 14 | 15 | 19 | 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /api/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 | this.transporter.use( 24 | 'compile', 25 | nodemailerhbs({ 26 | viewEngine: exphbs.create({ 27 | layoutsDir: resolve(viewPath, 'layouts'), 28 | partialsDir: resolve(viewPath, 'partials'), 29 | defaultLayout: 'default', 30 | extname: '.hbs', 31 | }), 32 | viewPath, 33 | extName: '.hbs', 34 | }) 35 | ); 36 | } 37 | 38 | sendMail(message) { 39 | return this.transporter.sendMail({ 40 | ...mailConfig.default, 41 | ...message, 42 | }); 43 | } 44 | } 45 | 46 | export default new Mail(); 47 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modulo2", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "nodemon src/server.js", 8 | "queue": "nodemon src/queue.js" 9 | }, 10 | "devDependencies": { 11 | "eslint": "^6.2.2", 12 | "eslint-config-airbnb-base": "^14.0.0", 13 | "eslint-config-prettier": "^6.1.0", 14 | "eslint-plugin-import": "^2.18.2", 15 | "eslint-plugin-prettier": "^3.1.0", 16 | "nodemon": "^1.19.1", 17 | "prettier": "^1.18.2", 18 | "sequelize-cli": "^5.5.0", 19 | "sucrase": "^3.10.1" 20 | }, 21 | "dependencies": { 22 | "@sentry/node": "5.6.2", 23 | "bcryptjs": "^2.4.3", 24 | "bee-queue": "^1.2.2", 25 | "cors": "^2.8.5", 26 | "date-fns": "^2.0.0-beta.5", 27 | "dotenv": "^8.1.0", 28 | "express": "^4.17.1", 29 | "express-async-errors": "^3.1.1", 30 | "express-handlebars": "^3.1.0", 31 | "ioredis": "^4.14.1", 32 | "jsonwebtoken": "^8.5.1", 33 | "mongoose": "^5.7.5", 34 | "multer": "^1.4.2", 35 | "nodemailer": "^6.3.0", 36 | "nodemailer-express-handlebars": "^3.1.0", 37 | "pg": "^7.12.1", 38 | "pg-hstore": "^2.3.3", 39 | "sequelize": "^5.16.0", 40 | "youch": "^2.0.10", 41 | "yup": "^0.27.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /mobile/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | }, 6 | extends: ['airbnb', 'prettier', 'prettier/react'], 7 | globals: { 8 | Atomics: 'readonly', 9 | SharedArrayBuffer: 'readonly', 10 | }, 11 | parser: 'babel-eslint', 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | ecmaVersion: 2018, 17 | sourceType: 'module', 18 | }, 19 | plugins: ['react', 'prettier', 'react-hooks'], 20 | rules: { 21 | "prettier/prettier": "error", 22 | "react/jsx-filename-extension": ["error", { extensions: [".js", ".jsx"] }], 23 | "import/prefer-default-export": "off", 24 | "no-unused-vars": ["error", { argsIgnorePattern: "^_" }], 25 | "react/jsx-one-expression-per-line": "off", 26 | "global-require": "off", 27 | "react-native/no-raw-text": "off", 28 | "no-param-reassign": "off", 29 | "no-underscore-dangle": "off", 30 | camelcase: "off", 31 | "no-console": ["error", { allow: ["tron"] }], 32 | "react-hooks/rules-of-hooks": "error", 33 | "react-hooks/exhaustive-deps": "warn" 34 | }, 35 | settings: { 36 | 'import/resolver': { 37 | 'babel-plugin-root-import': { 38 | rootPathSuffix: 'src', 39 | 40 | }, 41 | }, 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /api/src/database/migrations/20190904131019-create-appointments.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.createTable('appointments', { 4 | id: { 5 | type: Sequelize.INTEGER, 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | date: { 11 | type: Sequelize.DATE, 12 | allowNull: false, 13 | }, 14 | user_id: { 15 | type: Sequelize.INTEGER, 16 | references: { model: 'users', key: 'id' }, 17 | onUpdate: 'CASCADE', 18 | onDelete: 'SET NULL', 19 | allowNull: true, 20 | }, 21 | provider_id: { 22 | type: Sequelize.INTEGER, 23 | references: { model: 'users', key: 'id' }, 24 | onUpdate: 'CASCADE', 25 | onDelete: 'SET NULL', 26 | allowNull: true, 27 | }, 28 | canceled_at: { 29 | type: Sequelize.DATE, 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('appointments'); 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /web/src/components/header/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | background: #fff; 5 | padding: 0 30px; 6 | `; 7 | 8 | export const Content = styled.div` 9 | height: 64px; 10 | max-width: 900px; 11 | margin: 0 auto; 12 | display: flex; 13 | justify-content: space-between; 14 | align-items: center; 15 | 16 | nav { 17 | display: flex; 18 | align-items: center; 19 | 20 | img { 21 | margin-right: 20px; 22 | padding-right: 20px; 23 | border-right: 1px solid #eee; 24 | } 25 | 26 | a { 27 | font-weight: bold; 28 | color: #7159c1; 29 | } 30 | } 31 | 32 | aside { 33 | display: flex; 34 | align-items: center; 35 | } 36 | `; 37 | 38 | export const Profile = styled.div` 39 | display: flex; 40 | margin-left: 20px; 41 | padding-left: 20px; 42 | border-left: 1px solid #eee; 43 | 44 | div { 45 | text-align: right; 46 | margin-right: 10px; 47 | 48 | strong { 49 | display: block; 50 | color: #333; 51 | } 52 | 53 | a { 54 | display: back; 55 | margin-top: 2px; 56 | font-size: 12px; 57 | color: #999; 58 | } 59 | } 60 | img { 61 | width: 32px; 62 | height: 32px; 63 | border-radius: 50%; 64 | } 65 | `; 66 | -------------------------------------------------------------------------------- /web/src/pages/SignIn/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import { Form, Input } from '@rocketseat/unform'; 5 | import * as Yup from 'yup'; 6 | 7 | import { signInRequest } from '~/store/modules/auth/actions'; 8 | 9 | import logo from '~/assets/logo.svg'; 10 | 11 | const schema = Yup.object().shape({ 12 | email: Yup.string() 13 | .email('Insira um email valido!') 14 | .required('O email é obrigatorio'), 15 | password: Yup.string().required('A senha é obrigatoria'), 16 | }); 17 | 18 | export default function SignIn() { 19 | const dispatch = useDispatch(); 20 | const loading = useSelector(state => state.auth.loading); 21 | 22 | function handleSubmit({ email, password }) { 23 | dispatch(signInRequest(email, password)); 24 | } 25 | return ( 26 | <> 27 | GoBarber 28 |
29 | 30 | 35 | 36 | Criar conta gratuita 37 |
38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /mobile/src/components/Appointment/index.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { parseISO, formatRelative } from 'date-fns'; 3 | import pt from 'date-fns/locale/pt'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import { TouchableOpacity } from 'react-native'; 7 | import Icon from 'react-native-vector-icons/MaterialIcons'; 8 | 9 | import { Container, Left, Avatar, Info, Name, Time } from './styles'; 10 | 11 | export default function Appointment({ data, onCancel }) { 12 | const dateParsed = useMemo(() => { 13 | return formatRelative(parseISO(data.date), new Date(), { 14 | locale: pt, 15 | addSuffix: true, 16 | }); 17 | }, [data.date]); 18 | return ( 19 | 20 | 21 | 28 | 29 | {data.provider.name} 30 | 31 | 32 | 33 | {data.cancelable && !data.canceled_at && ( 34 | 35 | 36 | 37 | )} 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /api/src/app.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import express from 'express'; 3 | import path from 'path'; 4 | import cors from 'cors'; 5 | import Youch from 'youch'; 6 | import * as Sentry from '@sentry/node'; 7 | import 'express-async-errors'; 8 | import routes from './routes'; 9 | import sentryConfig from './config/sentry'; 10 | 11 | import './database'; 12 | 13 | class App { 14 | constructor() { 15 | this.server = express(); 16 | 17 | Sentry.init(sentryConfig); 18 | 19 | this.middlewares(); 20 | this.routes(); 21 | this.exceptionHandler(); 22 | } 23 | 24 | middlewares() { 25 | this.server.use(Sentry.Handlers.requestHandler()); 26 | this.server.use(cors()); 27 | this.server.use(express.json()); 28 | this.server.use( 29 | '/files', 30 | express.static(path.resolve(__dirname, '..', 'tmp', 'uploads')) 31 | ); 32 | } 33 | 34 | routes() { 35 | this.server.use(routes); 36 | this.server.use(Sentry.Handlers.errorHandler()); 37 | } 38 | 39 | exceptionHandler() { 40 | this.server.use(async (err, req, res, next) => { 41 | if (process.env.NODE_ENV === 'development') { 42 | const errors = await new Youch(err, req).toJSON(); 43 | return res.status(500).json(errors); 44 | } 45 | return res.status(500).json({ error: 'internal server error' }); 46 | }); 47 | } 48 | } 49 | 50 | export default new App().server; 51 | -------------------------------------------------------------------------------- /web/src/pages/SignUp/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import * as Yup from 'yup'; 5 | import { Form, Input } from '@rocketseat/unform'; 6 | import { signUpRequest } from '~/store/modules/auth/actions'; 7 | 8 | import logo from '~/assets/logo.svg'; 9 | 10 | const schema = Yup.object().shape({ 11 | name: Yup.string().required('O nome é obrigatorio'), 12 | email: Yup.string() 13 | .email('Insira um email valido!') 14 | .required('O email é obrigatorio'), 15 | password: Yup.string() 16 | .min(6, 'Mínimo 6 caracteres') 17 | .required('A senha é obrigatoria'), 18 | }); 19 | 20 | export default function SignUp() { 21 | const dispatch = useDispatch(); 22 | 23 | function handleSubmit({ name, email, password }) { 24 | dispatch(signUpRequest(name, email, password)); 25 | } 26 | 27 | return ( 28 | <> 29 | GoBarber 30 |
31 | 32 | 33 | 38 | 39 | 40 | Já possuo uma conta 41 |
42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /api/src/app/services/AvailableService.js: -------------------------------------------------------------------------------- 1 | import { 2 | startOfDay, 3 | endOfDay, 4 | setHours, 5 | setMinutes, 6 | setSeconds, 7 | format, 8 | isAfter, 9 | } from 'date-fns'; 10 | 11 | import { Op } from 'sequelize'; 12 | import Appointment from '../models/Appointment'; 13 | 14 | class AvailableService { 15 | async run({ provider_id, date }) { 16 | const appointments = await Appointment.findAll({ 17 | where: { 18 | provider_id, 19 | canceled_at: null, 20 | date: { 21 | [Op.between]: [startOfDay(date), endOfDay(date)], 22 | }, 23 | }, 24 | }); 25 | 26 | const schedule = [ 27 | '08:00', 28 | '09:00', 29 | '10:00', 30 | '11:00', 31 | '12:00', 32 | '13:00', 33 | '14:00', 34 | '15:00', 35 | '16:00', 36 | '17:00', 37 | '18:00', 38 | '19:00', 39 | '20:00', 40 | '21:00', 41 | ]; 42 | 43 | const available = schedule.map(time => { 44 | const [hour, minute] = time.split(':'); 45 | const value = setSeconds(setMinutes(setHours(date, hour), minute), 0); 46 | return { 47 | time, 48 | value: format(value, "yyyy-MM-dd'T'HH:mm:ssXXX"), 49 | available: 50 | isAfter(value, new Date()) && 51 | !appointments.find(a => format(a.date, 'HH:mm') === time), 52 | }; 53 | }); 54 | 55 | return available; 56 | } 57 | } 58 | 59 | export default new AvailableService(); 60 | -------------------------------------------------------------------------------- /web/src/pages/Profile/AvatarInput/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | import React, { useState, useRef, useEffect } from 'react'; 3 | import { useField } from '@rocketseat/unform'; 4 | import api from '~/services/api'; 5 | 6 | import { Container } from './styles'; 7 | 8 | export default function AvatarInput() { 9 | const { defaultValue, registerField } = useField('avatar'); 10 | 11 | const [file, setFile] = useState(defaultValue && defaultValue.id); 12 | const [preview, setPreview] = useState(defaultValue && defaultValue.url); 13 | 14 | const ref = useRef(); 15 | 16 | useEffect(() => { 17 | if (ref.current) { 18 | registerField({ 19 | name: 'avatar_id', 20 | ref: ref.current, 21 | path: 'dataset.file', 22 | }); 23 | } 24 | }, [ref.current]); 25 | 26 | async function handleChange(e) { 27 | const data = new FormData(); 28 | 29 | data.append('file', e.target.files[0]); 30 | 31 | const response = await api.post('files', data); 32 | 33 | const { id, url } = response.data; 34 | 35 | setFile(id); 36 | setPreview(url); 37 | } 38 | 39 | return ( 40 | 41 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /api/src/app/services/CancelAppointmentService.js: -------------------------------------------------------------------------------- 1 | import { isBefore, subHours } from 'date-fns'; 2 | 3 | import Appointment from '../models/Appointment'; 4 | import User from '../models/User'; 5 | 6 | import CancellationMail from '../jobs/CancellationMail'; 7 | import Queue from '../../lib/Queue'; 8 | import Cache from '../../lib/Cache'; 9 | 10 | class CancelAppointment { 11 | async run({ provider_id, user_id }) { 12 | const appointment = await Appointment.findByPk(provider_id, { 13 | include: [ 14 | { 15 | model: User, 16 | as: 'provider', 17 | attributes: ['name', 'email'], 18 | }, 19 | { 20 | model: User, 21 | as: 'user', 22 | attributes: ['name'], 23 | }, 24 | ], 25 | }); 26 | if (appointment.user_id !== user_id) { 27 | throw new Error("You don't have permition to cancel this appointment"); 28 | } 29 | const dateWithSub = subHours(appointment.date, 2); 30 | 31 | if (isBefore(dateWithSub, new Date())) { 32 | throw new Error(' you can only cancel appointments 2 hours in advance'); 33 | } 34 | 35 | appointment.canceled_at = new Date(); 36 | await appointment.save(); 37 | 38 | await Queue.add(CancellationMail.key, { 39 | appointment, 40 | }); 41 | /** 42 | * Invalidate cache 43 | */ 44 | await Cache.invalidatePrefix(`user:${user_id}:appointments`); 45 | 46 | return appointment; 47 | } 48 | } 49 | 50 | export default new CancelAppointment(); 51 | -------------------------------------------------------------------------------- /api/src/app/controllers/SessionController.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import * as Yup from 'yup'; 3 | import User from '../models/User'; 4 | import File from '../models/File'; 5 | import authConfig from '../../config/auth'; 6 | 7 | class SessionController { 8 | async store(req, res) { 9 | const schema = Yup.object().shape({ 10 | email: Yup.string() 11 | .email() 12 | .required(), 13 | password: Yup.string().required(), 14 | }); 15 | if (!(await schema.isValid(req.body))) { 16 | return res.status(400).json({ error: 'Validation fails' }); 17 | } 18 | const { email, password } = req.body; 19 | const user = await User.findOne({ 20 | where: { email }, 21 | include: [ 22 | { 23 | model: File, 24 | as: 'avatar', 25 | attributes: ['id', 'path', 'url'], 26 | }, 27 | ], 28 | }); 29 | 30 | if (!user) { 31 | return res.status(401).json({ error: 'User not found' }); 32 | } 33 | if (!(await user.checkPassword(password))) { 34 | return res.json({ error: 'Password does not match' }); 35 | } 36 | const { id, name, avatar, provider } = user; 37 | return res.json({ 38 | user: { 39 | id, 40 | name, 41 | email, 42 | avatar, 43 | provider, 44 | }, 45 | token: jwt.sign({ id }, authConfig.secret, { 46 | expiresIn: authConfig.expiresIn, 47 | }), 48 | }); 49 | } 50 | } 51 | 52 | export default new SessionController(); 53 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /web/src/pages/_layouts/auth/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { darken } from 'polished'; 3 | 4 | export const Wrapper = styled.div` 5 | height: 100%; 6 | background: linear-gradient(-90deg, #7159c1, #ab59c1); 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | `; 11 | export const Content = styled.div` 12 | width: 100%; 13 | max-width: 315px; 14 | text-align: center; 15 | 16 | form { 17 | display: flex; 18 | flex-direction: column; 19 | margin-top: 30px; 20 | 21 | input { 22 | background: rgba(0, 0, 0, 0.1); 23 | border: 0; 24 | border-radius: 4px; 25 | height: 44px; 26 | padding: 0 15px; 27 | color: #fff; 28 | margin: 0 0 10px; 29 | 30 | &::placeholder { 31 | color: rgba(255, 255, 255, 0.7); 32 | } 33 | } 34 | span { 35 | color: #fb6f91; 36 | align-self: flex-start; 37 | margin: 0 0 10px; 38 | font-weight: bold; 39 | } 40 | 41 | button { 42 | margin: 5px 0 0; 43 | height: 44px; 44 | background: #3d9eff; 45 | font-weight: bold; 46 | color: #fff; 47 | border: 0; 48 | border-radius: 4px; 49 | font-size: 16px; 50 | transition: background 0.2s ease; 51 | 52 | &:hover { 53 | background: ${darken(0.03, '#3d9eff')}; 54 | } 55 | } 56 | 57 | a { 58 | color: #fff; 59 | margin-top: 15px; 60 | font-size: 16px; 61 | opacity: 0.8; 62 | 63 | &:hover { 64 | opacity: 1; 65 | } 66 | } 67 | } 68 | `; 69 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /web/src/pages/Profile/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { Form, Input } from '@rocketseat/unform'; 4 | 5 | import { updateProfileRequest } from '~/store/modules/user/actions'; 6 | import { signOut } from '~/store/modules/auth/actions'; 7 | import AvatarInput from './AvatarInput'; 8 | import { Container } from './styles'; 9 | 10 | export default function Profile() { 11 | const dispatch = useDispatch(); 12 | const profile = useSelector(state => state.user.profile); 13 | 14 | function handleSubmit(data) { 15 | dispatch(updateProfileRequest(data)); 16 | } 17 | function handleSignOut() { 18 | dispatch(signOut()); 19 | } 20 | 21 | return ( 22 | 23 |
24 | 25 | 26 | 27 | 28 |
29 | 30 | 35 | 36 | 41 | 42 | 43 | 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /web/src/pages/Profile/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { darken } from 'polished'; 3 | 4 | export const Container = styled.div` 5 | max-width: 600px; 6 | margin: 50px auto; 7 | 8 | form { 9 | display: flex; 10 | flex-direction: column; 11 | margin-top: 30px; 12 | 13 | input { 14 | background: rgba(0, 0, 0, 0.1); 15 | border: 0; 16 | border-radius: 4px; 17 | height: 44px; 18 | padding: 0 15px; 19 | color: #fff; 20 | margin: 0 0 10px; 21 | 22 | &::placeholder { 23 | color: rgba(255, 255, 255, 0.7); 24 | } 25 | } 26 | span { 27 | color: #fb6f91; 28 | align-self: flex-start; 29 | margin: 0 0 10px; 30 | font-weight: bold; 31 | } 32 | 33 | hr { 34 | background: rgba(255, 255, 255, 0.5); 35 | margin: 10px 0 20px; 36 | height: 1px; 37 | border: 0; 38 | } 39 | 40 | button { 41 | margin: 5px 0 0; 42 | height: 44px; 43 | background: #3d9eff; 44 | font-weight: bold; 45 | color: #fff; 46 | border: 0; 47 | border-radius: 4px; 48 | font-size: 16px; 49 | transition: background 0.2s ease; 50 | 51 | &:hover { 52 | background: ${darken(0.03, '#3d9eff')}; 53 | } 54 | } 55 | } 56 | > button { 57 | width: 100%; 58 | margin: 10px 0 0; 59 | height: 44px; 60 | background: #f64c75; 61 | font-weight: bold; 62 | color: #fff; 63 | border: 0; 64 | border-radius: 4px; 65 | font-size: 16px; 66 | transition: background 0.2s ease; 67 | 68 | &:hover { 69 | background: ${darken(0.08, '#f64c75')}; 70 | } 71 | } 72 | `; 73 | -------------------------------------------------------------------------------- /api/src/routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import multer from 'multer'; 3 | import multerConfig from './config/multer'; 4 | 5 | import UserController from './app/controllers/UserController'; 6 | import SessionController from './app/controllers/SessionController'; 7 | 8 | import FileController from './app/controllers/FileController'; 9 | import ProviderController from './app/controllers/ProviderController'; 10 | import AppointmentController from './app/controllers/AppointmentController'; 11 | import ScheduleController from './app/controllers/ScheduleController'; 12 | import AvailableController from './app/controllers/AvailableController'; 13 | 14 | import NotificationController from './app/controllers/NotificationController'; 15 | 16 | import authMiddleware from './app/middlewares/auth'; 17 | 18 | const routes = new Router(); 19 | const upload = multer(multerConfig); 20 | 21 | routes.post('/users', UserController.store); 22 | routes.post('/sessions', SessionController.store); 23 | 24 | routes.use(authMiddleware); 25 | 26 | routes.put('/users', UserController.update); 27 | 28 | routes.get('/providers', ProviderController.index); 29 | routes.get('/providers/:providerId/available', AvailableController.index); 30 | 31 | routes.get('/appointments', AppointmentController.index); 32 | routes.post('/appointments', AppointmentController.store); 33 | routes.delete('/appointments/:id', AppointmentController.delete); 34 | 35 | routes.get('/schedule', ScheduleController.index); 36 | routes.get('/notifications', NotificationController.index); 37 | routes.put('/notifications/:id', NotificationController.update); 38 | 39 | routes.post('/files', upload.single('file'), FileController.store); 40 | 41 | export default routes; 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /web/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | logo 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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 | 57 | 58 | -------------------------------------------------------------------------------- /mobile/src/pages/Dashboard/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import Icon from 'react-native-vector-icons/MaterialIcons'; 3 | import { withNavigationFocus } from 'react-navigation'; 4 | import Background from '~/components/Background'; 5 | 6 | import api from '~/services/api'; 7 | 8 | import Appointment from '~/components/Appointment'; 9 | 10 | import { Container, Title, List } from './styles'; 11 | 12 | function Dashboard({ isFocused }) { 13 | const [appointments, setAppointments] = useState([]); 14 | 15 | async function handleCancel(id) { 16 | const response = await api.delete(`appointments/${id}`); 17 | setAppointments( 18 | appointments.map(appointment => 19 | appointment.id === id 20 | ? { 21 | ...appointment, 22 | canceled_at: response.data.canceled_at, 23 | } 24 | : appointment 25 | ) 26 | ); 27 | } 28 | async function loadAppointments() { 29 | const response = await api.get('appointments'); 30 | setAppointments(response.data); 31 | } 32 | 33 | useEffect(() => { 34 | if (isFocused) { 35 | loadAppointments(); 36 | } 37 | }, [isFocused]); 38 | 39 | return ( 40 | 41 | 42 | Agendamentos 43 | 44 | String(item.id)} 47 | renderItem={({ item }) => ( 48 | handleCancel(item.id)} data={item} /> 49 | )} 50 | /> 51 | 52 | 53 | ); 54 | } 55 | 56 | Dashboard.navigationOptions = { 57 | tabBarLabel: 'Agendamentos', 58 | tabBarIcon: ({ tintColor }) => ( 59 | 60 | ), 61 | }; 62 | 63 | export default withNavigationFocus(Dashboard); 64 | -------------------------------------------------------------------------------- /web/src/store/modules/auth/sagas.js: -------------------------------------------------------------------------------- 1 | import { takeLatest, call, put, all } from 'redux-saga/effects'; 2 | import { toast } from 'react-toastify'; 3 | 4 | import history from '~/services/history'; 5 | import api from '~/services/api'; 6 | 7 | import { signInSuccess, signFailure } from './actions'; 8 | 9 | export function* signIn({ payload }) { 10 | try { 11 | const { email, password } = payload; 12 | const response = yield call(api.post, 'sessions', { 13 | email, 14 | password, 15 | }); 16 | 17 | const { token, user } = response.data; 18 | 19 | if (!user.provider) { 20 | toast.error('Usuario nao é prestador'); 21 | return; 22 | } 23 | 24 | api.defaults.headers.Authorization = ` Bearer ${token}`; 25 | 26 | yield put(signInSuccess(token, user)); 27 | 28 | history.push('/dashboard'); 29 | } catch (err) { 30 | toast.error('Usuario ou senha invalido'); 31 | yield put(signFailure()); 32 | } 33 | } 34 | 35 | export function* signUp({ payload }) { 36 | try { 37 | const { name, email, password } = payload; 38 | yield call(api.post, 'users', { 39 | name, 40 | email, 41 | password, 42 | provider: true, 43 | }); 44 | history.push('/'); 45 | } catch (err) { 46 | toast.error('Falha no cadastro seus dados'); 47 | 48 | yield put(signFailure()); 49 | } 50 | } 51 | export function setToken({ payload }) { 52 | if (!payload) return; 53 | 54 | const { token } = payload.auth; 55 | 56 | if (token) { 57 | api.defaults.headers.Authorization = ` Bearer ${token}`; 58 | } 59 | } 60 | export function signOut() { 61 | history.push('/'); 62 | } 63 | 64 | export default all([ 65 | takeLatest('persist/REHYDRATE', setToken), 66 | takeLatest('@auth/SIGN_IN_REQUEST', signIn), 67 | takeLatest('@auth/SIGN_UP_REQUEST', signUp), 68 | takeLatest('@auth/SIGN_OUT', signOut), 69 | ]); 70 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 |

GoBarber

2 | 3 | Instructions 4 |
    5 |
  • First step: inside api's folder, run : "Docker-compose up" to create containers
  • 6 |
  • Second step: inside api's folder, run: " yarn sequelize db:migrate" to create the table in your database
  • 7 |
  • Third step: inside api's folder run: " yarn dev " to start the back-end
  • 8 |
  • Fourth step: inside api's folder run: " yarn queue " to start the background job
  • 9 |
  • Fifth step: inside web folder, run "yarn start" to start the front-end
  • 10 |
  • You can also use NPM
  • 11 |
12 |

Backend

13 |

This is an api rest that i built with ExpressJS and i used the following DBs:
14 | PostgreSQL, MongoDB and Redis.
15 | ORM: Sequelize and Mongoose.

16 | Read more
17 |

18 | 19 |

20 |
21 | 22 |

I worked with ReactJS, Redux and Redux-Saga

23 |

It's a Single page application only for providers, here they can read notifications and controll theier schedule.

24 | Read more
25 |

26 | 27 | 28 | 29 | 30 | 31 | 32 |

33 | 34 |
35 | 36 |

I worked with ReactNative

37 |

This app is only for clients, here they can select your provider and achedule a service

38 | Read more
39 | 40 |

41 | 42 | 43 |

44 | -------------------------------------------------------------------------------- /mobile/src/pages/Confirm/index.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { TouchableOpacity } from 'react-native'; 3 | import { formatRelative, parseISO } from 'date-fns'; 4 | 5 | import pt from 'date-fns/locale/pt'; 6 | import Icon from 'react-native-vector-icons/MaterialIcons'; 7 | 8 | import api from '~/services/api'; 9 | 10 | import Background from '~/components/Background'; 11 | 12 | import { Container, Avatar, Name, Time, SubmitButton } from './styles'; 13 | 14 | export default function Confirm({ navigation }) { 15 | const provider = navigation.getParam('provider'); 16 | const time = navigation.getParam('time'); 17 | const dateFormated = useMemo( 18 | () => formatRelative(parseISO(time), new Date(), { locale: pt }), 19 | [time] 20 | ); 21 | 22 | console.tron.log(dateFormated); 23 | 24 | async function handleAddAppointment() { 25 | await api.post('appointments', { 26 | provider_id: provider.id, 27 | date: time, 28 | }); 29 | navigation.navigate('Dashboard'); 30 | } 31 | return ( 32 | 33 | 34 | 41 | {provider.name} 42 | 43 | 44 | Confirmar agendamento 45 | 46 | 47 | 48 | ); 49 | } 50 | 51 | Confirm.navigationOptions = ({ navigation }) => ({ 52 | title: 'Confirmar agendamento', 53 | headerLeft: () => ( 54 | { 56 | navigation.goBack(); 57 | }} 58 | > 59 | 60 | 61 | ), 62 | }); 63 | -------------------------------------------------------------------------------- /mobile/src/pages/SelectProvider/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { TouchableOpacity } from 'react-native'; 3 | import Icon from 'react-native-vector-icons/MaterialIcons'; 4 | import api from '~/services/api'; 5 | import Background from '~/components/Background'; 6 | 7 | import { Container, ProvidersList, Provider, Avatar, Name } from './styles'; 8 | 9 | export default function SelectProvider({ navigation }) { 10 | const [providers, setProviders] = useState([]); 11 | 12 | useEffect(() => { 13 | async function loadProviders() { 14 | const response = await api.get('providers'); 15 | setProviders(response.data); 16 | } 17 | loadProviders(); 18 | }, []); 19 | return ( 20 | 21 | 22 | String(provider.id)} 25 | renderItem={({ item: provider }) => ( 26 | 28 | navigation.navigate('SelectDateTime', { provider }) 29 | } 30 | > 31 | 38 | 39 | {provider.name} 40 | 41 | )} 42 | /> 43 | 44 | 45 | ); 46 | } 47 | 48 | SelectProvider.navigationOptions = ({ navigation }) => ({ 49 | title: 'Selecione o prestador', 50 | headerLeft: () => ( 51 | { 53 | navigation.navigate('Dashboard'); 54 | }} 55 | > 56 | 57 | 58 | ), 59 | }); 60 | -------------------------------------------------------------------------------- /mobile/src/store/modules/auth/sagas.js: -------------------------------------------------------------------------------- 1 | import { takeLatest, call, put, all } from 'redux-saga/effects'; 2 | 3 | import { Alert } from 'react-native'; 4 | 5 | import api from '~/services/api'; 6 | 7 | import { signInSuccess, signFailure } from './actions'; 8 | 9 | export function* signIn({ payload }) { 10 | try { 11 | const { email, password } = payload; 12 | const response = yield call(api.post, 'sessions', { 13 | email, 14 | password, 15 | }); 16 | 17 | const { token, user } = response.data; 18 | 19 | if (user.provider) { 20 | Alert.alert( 21 | 'Erro no login', 22 | 'O usuario não pode ser um prestador de serviços' 23 | ); 24 | yield put(signFailure()); 25 | return; 26 | } 27 | 28 | api.defaults.headers.Authorization = ` Bearer ${token}`; 29 | 30 | yield put(signInSuccess(token, user)); 31 | 32 | // history.push('/dashboard'); 33 | } catch (err) { 34 | Alert.alert('Falha na autenticação', 'Usuario e/ou senha invalido'); 35 | yield put(signFailure()); 36 | } 37 | } 38 | 39 | export function* signUp({ payload }) { 40 | try { 41 | const { name, email, password } = payload; 42 | yield call(api.post, 'users', { 43 | name, 44 | email, 45 | password, 46 | }); 47 | Alert.alert('Sucesso !', 'Cadastro efetuado com sucesso!'); 48 | // history.push('/'); 49 | } catch (err) { 50 | Alert.alert( 51 | 'Falha no cadastro', 52 | 'Houve um erro no cadastro, verifique seus dados' 53 | ); 54 | 55 | yield put(signFailure()); 56 | } 57 | } 58 | export function setToken({ payload }) { 59 | if (!payload) return; 60 | 61 | const { token } = payload.auth; 62 | 63 | if (token) { 64 | api.defaults.headers.Authorization = ` Bearer ${token}`; 65 | } 66 | } 67 | 68 | export default all([ 69 | takeLatest('persist/REHYDRATE', setToken), 70 | takeLatest('@auth/SIGN_IN_REQUEST', signIn), 71 | takeLatest('@auth/SIGN_UP_REQUEST', signUp), 72 | ]); 73 | -------------------------------------------------------------------------------- /mobile/src/pages/SelectDateTime/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Icon from 'react-native-vector-icons/MaterialIcons'; 3 | import { TouchableOpacity } from 'react-native'; 4 | 5 | import api from '~/services/api'; 6 | 7 | import Background from '~/components/Background'; 8 | import DateInput from '~/components/DateInput'; 9 | 10 | import { Container, HourList, Hour, Title, Time } from './styles'; 11 | 12 | export default function SelectDateTime({ navigation }) { 13 | const [date, setDate] = useState(new Date()); 14 | const [hours, setHours] = useState([]); 15 | 16 | const provider = navigation.getParam('provider'); 17 | useEffect(() => { 18 | async function loadAvailable() { 19 | const response = await api.get(`providers/${provider.id}/available`, { 20 | params: { 21 | date: date.getTime(), 22 | }, 23 | }); 24 | setHours(response.data); 25 | } 26 | loadAvailable(); 27 | }, [date, provider.id]); 28 | 29 | function handleSelectHour(time) { 30 | navigation.navigate('Confirm', { 31 | provider, 32 | time, 33 | }); 34 | } 35 | 36 | return ( 37 | 38 | 39 | 40 | item.time} 43 | renderItem={({ item }) => ( 44 | handleSelectHour(item.value)} 47 | > 48 | {item.time} 49 | 50 | )} 51 | /> 52 | 53 | 54 | ); 55 | } 56 | 57 | SelectDateTime.navigationOptions = ({ navigation }) => ({ 58 | title: 'Selecione o Horário', 59 | headerLeft: () => ( 60 | { 62 | navigation.goBack(); 63 | }} 64 | > 65 | 66 | 67 | ), 68 | }); 69 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "estruturareact", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@rocketseat/unform": "^1.6.1", 7 | "axios": "^0.19.0", 8 | "date-fns": "^2.0.0-beta.5", 9 | "date-fns-tz": "^1.0.8", 10 | "history": "^4.10.1", 11 | "immer": "^4.0.2", 12 | "polished": "^3.4.2", 13 | "prop-types": "^15.7.2", 14 | "react": "^16.11.0", 15 | "react-dom": "^16.11.0", 16 | "react-icons": "^3.8.0", 17 | "react-perfect-scrollbar": "^1.5.3", 18 | "react-redux": "^7.1.1", 19 | "react-router-dom": "^5.1.2", 20 | "react-scripts": "3.2.0", 21 | "react-toastify": "^5.4.0", 22 | "reactotron-react-js": "^3.3.4", 23 | "reactotron-redux": "^3.1.2", 24 | "reactotron-redux-saga": "^4.2.2", 25 | "redux": "^4.0.4", 26 | "redux-persist": "^6.0.0", 27 | "redux-saga": "^1.1.1", 28 | "styled-components": "^4.4.1", 29 | "yup": "^0.27.0" 30 | }, 31 | "scripts": { 32 | "start": "react-app-rewired start", 33 | "build": "react-app-rewired build", 34 | "test": "react-app-rewired test", 35 | "eject": "react-scripts eject" 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.2%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | }, 49 | "devDependencies": { 50 | "babel-eslint": "^10.0.3", 51 | "babel-plugin-root-import": "^6.4.1", 52 | "customize-cra": "^0.8.0", 53 | "eslint": "^6.6.0", 54 | "eslint-config-airbnb": "^18.0.1", 55 | "eslint-config-prettier": "^6.5.0", 56 | "eslint-import-resolver-babel-plugin-root-import": "^1.1.1", 57 | "eslint-plugin-import": "^2.18.2", 58 | "eslint-plugin-jsx-a11y": "^6.2.3", 59 | "eslint-plugin-prettier": "^3.1.1", 60 | "eslint-plugin-react": "^7.16.0", 61 | "eslint-plugin-react-hooks": "^2.2.0", 62 | "prettier": "^1.18.2", 63 | "react-app-rewired": "^2.1.4" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /mobile/src/pages/SignIn/index.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { Image } from 'react-native'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | 5 | import logo from '~/assets/logo.png'; 6 | 7 | import Background from '~/components/Background'; 8 | import { signInRequest } from '~/store/modules/auth/actions'; 9 | 10 | import { 11 | Container, 12 | Form, 13 | FormInput, 14 | SubmitButton, 15 | SignLink, 16 | SignLinkText, 17 | } from './styles'; 18 | 19 | export default function SignIn({ navigation }) { 20 | const loading = useSelector(state => state.auth.loading); 21 | const passwordRef = useRef(); 22 | const dispatch = useDispatch(); 23 | 24 | const [email, setEmail] = useState(''); 25 | const [password, setPassword] = useState(''); 26 | 27 | function handleSubmit() { 28 | dispatch(signInRequest(email, password)); 29 | } 30 | 31 | return ( 32 | 33 | 34 | 35 |
36 | passwordRef.current.focus()} 44 | value={email} 45 | onChangeText={setEmail} 46 | /> 47 | 57 | 58 | 59 | Acessar 60 | 61 | 62 | navigation.navigate('SignUp')}> 63 | Criar conta gratuita 64 | 65 |
66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /mobile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobile", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios", 8 | "start": "react-native start", 9 | "test": "jest", 10 | "lint": "eslint ." 11 | }, 12 | "dependencies": { 13 | "@react-native-community/async-storage": "^1.6.3", 14 | "axios": "^0.19.0", 15 | "date-fns": "^2.0.0-beta.5", 16 | "immer": "^5.0.0", 17 | "prop-types": "^15.7.2", 18 | "react": "16.9.0", 19 | "react-native": "0.61.4", 20 | "react-native-gesture-handler": "^1.5.0", 21 | "react-native-linear-gradient": "^2.5.6", 22 | "react-native-reanimated": "^1.4.0", 23 | "react-native-screens": "^1.0.0-alpha.23", 24 | "react-native-vector-icons": "^6.6.0", 25 | "react-native-webview": "^7.5.1", 26 | "react-navigation": "^4.0.10", 27 | "react-navigation-stack": "^1.10.3", 28 | "react-navigation-tabs": "^2.5.6", 29 | "react-redux": "^7.1.3", 30 | "reactotron-react-native": "^4.0.2", 31 | "reactotron-redux": "^3.1.2", 32 | "reactotron-redux-saga": "^4.2.2", 33 | "redux": "^4.0.4", 34 | "redux-persist": "^6.0.0", 35 | "redux-saga": "^1.1.3", 36 | "styled-components": "^4.4.1" 37 | }, 38 | "devDependencies": { 39 | "@babel/core": "^7.7.2", 40 | "@babel/runtime": "^7.7.2", 41 | "@react-native-community/eslint-config": "^0.0.5", 42 | "babel-eslint": "^10.0.3", 43 | "babel-jest": "^24.9.0", 44 | "babel-plugin-root-import": "^6.4.1", 45 | "eslint": "^6.6.0", 46 | "eslint-config-airbnb": "^18.0.1", 47 | "eslint-config-prettier": "^6.6.0", 48 | "eslint-import-resolver-babel-plugin-root-import": "^1.1.1", 49 | "eslint-plugin-import": "^2.18.2", 50 | "eslint-plugin-jsx-a11y": "^6.2.3", 51 | "eslint-plugin-prettier": "^3.1.1", 52 | "eslint-plugin-react": "^7.16.0", 53 | "eslint-plugin-react-hooks": "^1.7.0", 54 | "jest": "^24.9.0", 55 | "metro-react-native-babel-preset": "^0.57.0", 56 | "prettier": "^1.19.1", 57 | "react-test-renderer": "16.9.0" 58 | }, 59 | "jest": { 60 | "preset": "react-native" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /api/src/app/services/CreateAppointmentService.js: -------------------------------------------------------------------------------- 1 | import { startOfHour, parseISO, isBefore, format } from 'date-fns'; 2 | import pt from 'date-fns/locale/pt'; 3 | 4 | import User from '../models/User'; 5 | import Appointment from '../models/Appointment'; 6 | 7 | import Cache from '../../lib/Cache'; 8 | 9 | import Notification from '../schemas/Notification'; 10 | 11 | class CreateAppointmentService { 12 | async run({ provider_id, user_id, date }) { 13 | /* 14 | **check if provider is valid 15 | */ 16 | const CheckisProvider = await User.findOne({ 17 | where: { id: provider_id, provider: true }, 18 | }); 19 | 20 | if (!CheckisProvider) { 21 | throw new Error('You can only create appointments with providers'); 22 | } 23 | 24 | /* 25 | * check for past dates 26 | */ 27 | const hourStart = startOfHour(parseISO(date)); 28 | 29 | if (isBefore(hourStart, new Date())) { 30 | throw new Error('Past dates are not permitted'); 31 | } 32 | 33 | /* 34 | *check date availability 35 | */ 36 | 37 | const checkAvailability = await Appointment.findOne({ 38 | where: { 39 | provider_id, 40 | canceled_at: null, 41 | date: hourStart, 42 | }, 43 | }); 44 | 45 | if (user_id === provider_id) { 46 | throw new Error("You can't create schedules for yourself"); 47 | } 48 | if (checkAvailability) { 49 | throw new Error('Appointment date is not available'); 50 | } 51 | 52 | const appointment = await Appointment.create({ 53 | user_id, 54 | provider_id, 55 | date, 56 | }); 57 | 58 | /* 59 | ** Notify the provider 60 | */ 61 | const user = await User.findByPk(user_id); 62 | const formattedDate = format( 63 | hourStart, 64 | "'dia' dd 'de' MMMM', ás' H:mm'h'", 65 | { locale: pt } 66 | ); 67 | await Notification.create({ 68 | content: `Novo agendamento de ${user.name} para ${formattedDate}`, 69 | user: provider_id, 70 | }); 71 | 72 | /** 73 | * Invalidate cache 74 | */ 75 | await Cache.invalidatePrefix(`user:${user_id}:appointments`); 76 | 77 | return appointment; 78 | } 79 | } 80 | 81 | export default new CreateAppointmentService(); 82 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /web/src/assets/headerLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | logo 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /api/src/app/controllers/AppointmentController.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | import Appointment from '../models/Appointment'; 3 | import File from '../models/File'; 4 | import User from '../models/User'; 5 | 6 | import Cache from '../../lib/Cache'; 7 | 8 | import CreateAppointmentService from '../services/CreateAppointmentService'; 9 | import CancelAppointmentService from '../services/CancelAppointmentService'; 10 | 11 | class AppointmentController { 12 | async index(req, res) { 13 | const { page = 1 } = req.query; 14 | 15 | const cacheKey = `user:${req.userId}:appointments:${page}`; 16 | const cached = await Cache.get(cacheKey); 17 | 18 | if (cached) { 19 | return res.json(cached); 20 | } 21 | 22 | const appontments = await Appointment.findAll({ 23 | where: { 24 | user_id: req.userId, 25 | canceled_at: null, 26 | }, 27 | order: ['date'], 28 | limit: 20, 29 | offset: (page - 1) * 20, 30 | attributes: ['id', 'date', 'past', 'cancelable'], 31 | include: [ 32 | { 33 | model: User, 34 | as: 'provider', 35 | attributes: ['id', 'name'], 36 | include: [ 37 | { 38 | model: File, 39 | as: 'avatar', 40 | attributes: ['id', 'path', 'url'], 41 | }, 42 | ], 43 | }, 44 | ], 45 | }); 46 | await Cache.set(cacheKey, appontments); 47 | 48 | return res.json(appontments); 49 | } 50 | 51 | async store(req, res) { 52 | const schema = Yup.object().shape({ 53 | provider: Yup.number().required(), 54 | date: Yup.date().required(), 55 | }); 56 | 57 | if (await schema.isValid(req.body)) { 58 | return res.status(400).json({ error: 'Validation fails' }); 59 | } 60 | 61 | const { provider_id, date } = req.body; 62 | 63 | const appointment = await CreateAppointmentService.run({ 64 | provider_id, 65 | user_id: req.userId, 66 | date, 67 | }); 68 | 69 | return res.json(appointment); 70 | } 71 | 72 | async delete(req, res) { 73 | const appointment = await CancelAppointmentService.run({ 74 | provider_id: req.params.id, 75 | user_id: req.userId, 76 | }); 77 | 78 | return res.json(appointment); 79 | } 80 | } 81 | 82 | export default new AppointmentController(); 83 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /web/src/components/Notifications/styles.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import PerfectScrollBar from 'react-perfect-scrollbar'; 3 | import { lighten } from 'polished'; 4 | 5 | export const Container = styled.div` 6 | position: relative; 7 | `; 8 | 9 | export const Badge = styled.button` 10 | background: none; 11 | border: 0; 12 | position: relative; 13 | 14 | ${props => 15 | props.hasUnread && 16 | css` 17 | &::after { 18 | content: ''; 19 | position: absolute; 20 | right: 0; 21 | top: 0; 22 | width: 8px; 23 | height: 8px; 24 | background: #ff892e; 25 | border-radius: 50%; 26 | } 27 | `} 28 | `; 29 | 30 | export const NotificationList = styled.div` 31 | position: absolute; 32 | width: 260px; 33 | z-index: 1; 34 | left: calc(50% - 130px); 35 | top: calc(100% + 30px); 36 | background: rgba(0, 0, 0, 0.6); 37 | border-radius: 4px; 38 | padding: 15px 5px; 39 | display: ${props => (props.visible ? 'block' : 'none')}; 40 | 41 | &::before { 42 | content: ''; 43 | position: absolute; 44 | left: calc(50% - 20px); 45 | top: -20px; 46 | width: 0; 47 | height: 0; 48 | border-left: 20px solid transparent; 49 | border-right: 20px solid transparent; 50 | border-bottom: 20px solid rgba(0, 0, 0, 0.6); 51 | } 52 | `; 53 | 54 | export const Scroll = styled(PerfectScrollBar)` 55 | max-height: 260px; 56 | padding: 5px 15px; 57 | `; 58 | 59 | export const Notification = styled.div` 60 | color: #fff; 61 | & + div { 62 | padding-top: 15px; 63 | margin-top: 15px; 64 | border-top: 1px solid rgba(255, 255, 255, 0.1); 65 | } 66 | 67 | p { 68 | font-size: 13px; 69 | line-height: 18px; 70 | } 71 | 72 | time { 73 | font-size: 12px; 74 | opacity: 0.6; 75 | display: block; 76 | margin-bottom: 5px; 77 | margin-top: 2px; 78 | } 79 | 80 | button { 81 | font-size: 12px; 82 | border: 0; 83 | background: none; 84 | color: ${lighten(0.2, '#7159c1')}; 85 | } 86 | 87 | ${props => 88 | props.unread && 89 | css` 90 | &::after { 91 | content: ''; 92 | display: inline-block; 93 | width: 8px; 94 | height: 8px; 95 | background: #ff892e; 96 | border-radius: 50%; 97 | margin-left: 10px; 98 | } 99 | `} 100 | `; 101 | -------------------------------------------------------------------------------- /mobile/src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createAppContainer, createSwitchNavigator } from 'react-navigation'; 3 | import { createStackNavigator } from 'react-navigation-stack'; 4 | import { createBottomTabNavigator } from 'react-navigation-tabs'; 5 | 6 | import Icon from 'react-native-vector-icons/MaterialIcons'; 7 | 8 | import SignIn from './pages/SignIn'; 9 | import SignUp from './pages/SignUp'; 10 | 11 | import Dashboard from './pages/Dashboard'; 12 | import Profile from './pages/Profile'; 13 | import SelectProvider from './pages/SelectProvider'; 14 | import SelectDateTime from './pages/SelectDateTime'; 15 | import Confirm from './pages/Confirm'; 16 | 17 | export default (signedIn = false) => 18 | createAppContainer( 19 | createSwitchNavigator( 20 | { 21 | Sign: createSwitchNavigator({ 22 | SignIn, 23 | SignUp, 24 | }), 25 | App: createBottomTabNavigator( 26 | { 27 | Dashboard, 28 | New: { 29 | screen: createStackNavigator( 30 | { 31 | SelectProvider, 32 | SelectDateTime, 33 | Confirm, 34 | }, 35 | { 36 | headerLayoutPreset: 'center', 37 | defaultNavigationOptions: { 38 | headerTransparent: true, 39 | headerTintColor: '#fff', 40 | headerLeftContainerStyle: { 41 | marginLeft: 20, 42 | }, 43 | }, 44 | } 45 | ), 46 | navigationOptions: { 47 | tabBarVisible: false, 48 | tabBarLabel: 'Agendar', 49 | tabBarIcon: ( 50 | 55 | ), 56 | }, 57 | }, 58 | Profile, 59 | }, 60 | { 61 | resetOnBlur: true, 62 | tabBarOptions: { 63 | keyboardHidesTabBar: true, 64 | activeTintColor: '#fff', 65 | inactiveTintColor: 'rgba(255,255,255,0.6)', 66 | style: { 67 | backgroundColor: '#8d41a8', 68 | }, 69 | }, 70 | } 71 | ), 72 | }, 73 | { 74 | initialRouteName: signedIn ? 'App' : 'Sign', 75 | } 76 | ) 77 | ); 78 | -------------------------------------------------------------------------------- /web/src/components/Notifications/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo } from 'react'; 2 | import { MdNotifications } from 'react-icons/md'; 3 | import { parseISO, formatDistance } from 'date-fns'; 4 | import pt from 'date-fns/locale/pt'; 5 | 6 | import api from '~/services/api'; 7 | 8 | import { 9 | Container, 10 | Badge, 11 | NotificationList, 12 | Scroll, 13 | Notification, 14 | } from './styles'; 15 | 16 | export default function Notifications() { 17 | const [visible, setVisible] = useState(false); 18 | const [notifications, setNotifications] = useState([]); 19 | 20 | const hasUnread = useMemo( 21 | () => !!notifications.find(notification => notification.read === false), 22 | [notifications] 23 | ); 24 | 25 | useEffect(() => { 26 | async function loadNotifications() { 27 | const response = await api.get('notifications'); 28 | const data = response.data.map(notifications => ({ 29 | ...notifications, 30 | timeDistance: formatDistance( 31 | parseISO(notifications.createdAt), 32 | new Date(), 33 | { addSuffix: true, locale: pt } 34 | ), 35 | })); 36 | 37 | setNotifications(data); 38 | } 39 | 40 | loadNotifications(); 41 | }, []); 42 | 43 | function handleToggleVisible() { 44 | setVisible(!visible); 45 | } 46 | 47 | async function handleMarkAsRead(id) { 48 | await api.put(`notifications/${id}`); 49 | 50 | setNotifications( 51 | notifications.map(notification => 52 | notification._id === id ? { ...notification, read: true } : notification 53 | ) 54 | ); 55 | } 56 | 57 | return ( 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | {notifications.map(notification => ( 66 | 67 |

{notification.content}

68 | 69 | {!notification.read && ( 70 | 76 | )} 77 |
78 | ))} 79 |
80 |
81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /web/src/pages/Dashboard/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo, useEffect } from 'react'; 2 | import { 3 | format, 4 | subDays, 5 | addDays, 6 | setHours, 7 | setMinutes, 8 | setSeconds, 9 | isBefore, 10 | parseISO, 11 | } from 'date-fns'; 12 | import { utcToZonedTime } from 'date-fns-tz'; 13 | import pt from 'date-fns/locale/pt'; 14 | import { MdChevronLeft, MdChevronRight } from 'react-icons/md'; 15 | import api from '~/services/api'; 16 | 17 | import { Container, Time } from './styles'; 18 | 19 | const range = [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; 20 | 21 | export default function Dashboard() { 22 | const [schedule, setSchedule] = useState([]); 23 | const [date, setDate] = useState(new Date()); 24 | 25 | const dateFormated = useMemo( 26 | () => format(date, "d 'de' MMMM", { locale: pt }), 27 | [date] 28 | ); 29 | 30 | useEffect(() => { 31 | async function loadSchedule() { 32 | const response = await api.get('schedule', { 33 | params: { date }, 34 | }); 35 | 36 | const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 37 | 38 | const data = range.map(hour => { 39 | const checkDate = setSeconds(setMinutes(setHours(date, hour), 0), 0); 40 | 41 | const compareDate = utcToZonedTime(checkDate, timezone); 42 | 43 | return { 44 | time: `${hour}:00h`, 45 | past: isBefore(compareDate, new Date()), 46 | appointment: response.data.find( 47 | a => parseISO(a.date).toString() === compareDate.toString() 48 | ), 49 | }; 50 | }); 51 | 52 | setSchedule(data); 53 | } 54 | loadSchedule(); 55 | }, [date]); 56 | 57 | function handlePrevDay() { 58 | setDate(subDays(date, 1)); 59 | } 60 | function handleNextDay() { 61 | setDate(addDays(date, 1)); 62 | } 63 | 64 | return ( 65 | 66 |
67 | 70 | {dateFormated} 71 | 74 |
75 |
    76 | {schedule.map(time => ( 77 | 83 | ))} 84 |
85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /mobile/src/pages/SignUp/index.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { Image } from 'react-native'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { signUpRequest } from '~/store/modules/auth/actions'; 5 | 6 | import logo from '~/assets/logo.png'; 7 | 8 | import Background from '~/components/Background'; 9 | 10 | import { 11 | Container, 12 | Form, 13 | FormInput, 14 | SubmitButton, 15 | SignLink, 16 | SignLinkText, 17 | } from './styles'; 18 | 19 | export default function SignUp({ navigation }) { 20 | const emailRef = useRef(); 21 | const passwordRef = useRef(); 22 | const dispatch = useDispatch(); 23 | const loading = useSelector(state => state.auth.loading); 24 | 25 | const [name, setName] = useState(''); 26 | const [email, setEmail] = useState(''); 27 | const [password, setPassword] = useState(''); 28 | 29 | function handleSubmit() { 30 | dispatch(signUpRequest(name, email, password)); 31 | } 32 | 33 | return ( 34 | 35 | 36 | 37 |
38 | emailRef.current.focus()} 45 | value={name} 46 | onChangeText={setName} 47 | /> 48 | 49 | passwordRef.current.focus()} 58 | value={email} 59 | onChangeText={setEmail} 60 | /> 61 | 62 | 72 | 73 | 74 | Cadastrar 75 | 76 | 77 | navigation.navigate('SignIn')}> 78 | Já tenho uma conta 79 | 80 |
81 |
82 | ); 83 | } 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.BV.LinearGradient.LinearGradientPackage; 8 | import com.facebook.react.ReactNativeHost; 9 | import com.facebook.react.ReactPackage; 10 | import com.facebook.soloader.SoLoader; 11 | import java.lang.reflect.InvocationTargetException; 12 | import java.util.List; 13 | 14 | public class MainApplication extends Application implements ReactApplication { 15 | 16 | private final ReactNativeHost mReactNativeHost = 17 | new ReactNativeHost(this) { 18 | @Override 19 | public boolean getUseDeveloperSupport() { 20 | return BuildConfig.DEBUG; 21 | } 22 | 23 | @Override 24 | protected List getPackages() { 25 | @SuppressWarnings("UnnecessaryLocalVariable") 26 | List packages = new PackageList(this).getPackages(); 27 | // Packages that cannot be autolinked yet can be added manually here, for example: 28 | // packages.add(new MyReactNativePackage()); 29 | return packages; 30 | } 31 | 32 | @Override 33 | protected String getJSMainModuleName() { 34 | return "index"; 35 | } 36 | }; 37 | 38 | @Override 39 | public ReactNativeHost getReactNativeHost() { 40 | return mReactNativeHost; 41 | } 42 | 43 | @Override 44 | public void onCreate() { 45 | super.onCreate(); 46 | SoLoader.init(this, /* native exopackage */ false); 47 | initializeFlipper(this); // Remove this line if you don't want Flipper enabled 48 | } 49 | 50 | /** 51 | * Loads Flipper in React Native templates. 52 | * 53 | * @param context 54 | */ 55 | private static void initializeFlipper(Context context) { 56 | if (BuildConfig.DEBUG) { 57 | try { 58 | /* 59 | We use reflection here to pick up the class that initializes Flipper, 60 | since Flipper library is not available in release mode 61 | */ 62 | Class aClass = Class.forName("com.facebook.flipper.ReactNativeFlipper"); 63 | aClass.getMethod("initializeFlipper", Context.class).invoke(null, context); 64 | } catch (ClassNotFoundException e) { 65 | e.printStackTrace(); 66 | } catch (NoSuchMethodException e) { 67 | e.printStackTrace(); 68 | } catch (IllegalAccessException e) { 69 | e.printStackTrace(); 70 | } catch (InvocationTargetException e) { 71 | e.printStackTrace(); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /api/README.MD: -------------------------------------------------------------------------------- 1 |

API REST (GoBarber)

2 | 1: Run docker-compose up 3 | 2: Start the server run : yarn dev 4 | 5 |

Uma api desenvolvida com o framework não opinativo(ExpressJS) com o objetivo de 6 | gerenciar um barber shop.

7 |

Funcionalidades:

8 |

[Agendar horarios, Desmarcar Horarios], [Cadastrar e atualizar usuarios], [Gerenciar prestadores de serviços e suas agendas por data e hora], [notificar os prestadores sobre novos agendamentos], [sessoes controladas atráves do JWT]
9 |

Bancos de dados

10 |
PostgreSQL(Funcionalidades comuns e com multiplos relacionamentos)
11 |
MongoDB(Trabalhar com as notificações e verificação de "leitura")
12 |
Redis(Trabalhar com filas(backgroundJobs) durante o envio de emails)
13 | Docker
14 | Utilizado para criar o container com a imagem do PostgreSQL
15 | Utilizado para criar o container com a imagem do MongoDB
16 | Utilizado para criar o container com a imagem do Redis
17 | 18 |

Dependencies :

19 |
    20 | 21 |
  • 22 | BeeQueue
    23 | Utilizado para monitorar de filas background jobs 24 |
  • 25 | 26 |
  • 27 | Express
    28 | Framework não opinativo 29 |
  • 30 | 31 |
  • 32 | Nodemailer + Mailtrap
    33 | Utilizado para simular o envio de e-mail no ambiente de desenvolvimento 34 |
  • 35 | 36 |
  • 37 | Sequelize / Sequelize-cli
    38 | ORM utilizado fazer a abstração da base de dados 39 |
  • 40 |
  • 41 | Nodemon
    42 | ORM utilizado fazer a abstração da base de dados 43 |
  • 44 | 45 |
  • 46 | Bcrypt
    47 | utilizado para criptografar dados 48 |
  • 49 | 50 |
  • 51 | JWT
    52 | Json Web Token : Utilizado para criar a autenticação dos usuarios através de tokens 53 |
  • 54 | 55 |
  • 56 | Yup
    57 | Utilizado para fazer a validação dos dados de entrada 58 | 59 |
  • 60 | 61 |
  • 62 | Date-fns
    63 | Utilizado para fazer a validação datas 64 |
  • 65 | 66 |
  • 67 | multer
    68 | utilizado para lidar com multipart/form-data 69 |
  • 70 | 71 |
72 |

Dev dependencies :

73 |
    74 |
  • 75 | eslint
    76 |
  • 77 |
  • 78 | eslint-config-airbnb-base
    79 |
  • 80 |
  • 81 | eslint-config-prettier
    82 |
  • 83 |
  • 84 | eslint-plugin-import
    85 |
  • 86 |
  • 87 | sequelize-cli
    88 |
  • 89 |
  • 90 | nodemon
    91 |
  • 92 |
  • 93 | prettier
    94 |
  • 95 |
  • 96 | sucrase
    97 |
  • 98 |
99 | -------------------------------------------------------------------------------- /api/src/app/controllers/UserController.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | 3 | import User from '../models/User'; 4 | import File from '../models/File'; 5 | 6 | import Cache from '../../lib/Cache'; 7 | 8 | class UserController { 9 | async store(req, res) { 10 | const schema = Yup.object().shape({ 11 | name: Yup.string().required(), 12 | email: Yup.string() 13 | .email() 14 | .required(), 15 | password: Yup.string() 16 | .required() 17 | .min(6), 18 | }); 19 | if (!(await schema.isValid(req.body))) { 20 | return res.status(400).json({ error: 'Validation fails' }); 21 | } 22 | 23 | const userExists = await User.findOne({ where: { email: req.body.email } }); 24 | if (userExists) { 25 | return res.status(400).json({ error: 'user already exists.' }); 26 | } 27 | 28 | const { id, name, email, provider } = await User.create(req.body); 29 | 30 | if (provider) { 31 | await Cache.invalidate('providers'); 32 | } 33 | 34 | return res.json({ 35 | id, 36 | name, 37 | email, 38 | provider, 39 | }); 40 | } 41 | 42 | async update(req, res) { 43 | const schema = Yup.object().shape({ 44 | name: Yup.string(), 45 | email: Yup.string().email(), 46 | oldPassword: Yup.string().min(6), 47 | password: Yup.string() 48 | .min(6) 49 | .when('oldPassword', (oldPassword, field) => 50 | oldPassword ? field.required() : field 51 | ), 52 | confirmPassword: Yup.string().when('password', (password, field) => 53 | password ? field.required().oneOf([Yup.ref('password')]) : field 54 | ), 55 | }); 56 | if (!(await schema.isValid(req.body))) { 57 | return res.status(400).json({ error: 'Validation fails' }); 58 | } 59 | 60 | const { email, oldPassword } = req.body; 61 | const user = await User.findByPk(req.userId); 62 | 63 | if (email !== user.email) { 64 | const userExists = await User.findOne({ where: { email } }); 65 | if (userExists) { 66 | return res.status(400).json({ error: 'user already exists' }); 67 | } 68 | } 69 | 70 | if (oldPassword && !(await user.checkPassword(oldPassword))) { 71 | return res.status(401).json({ error: 'Password does not match' }); 72 | } 73 | await user.update(req.body); 74 | const { id, name, avatar } = await User.findByPk(req.userId, { 75 | include: [ 76 | { 77 | model: File, 78 | as: 'avatar', 79 | attributes: ['id', 'path', 'url'], 80 | }, 81 | ], 82 | }); 83 | 84 | return res.json({ 85 | id, 86 | name, 87 | email, 88 | avatar, 89 | }); 90 | } 91 | } 92 | 93 | export default new UserController(); 94 | -------------------------------------------------------------------------------- /mobile/ios/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' 3 | 4 | target 'mobile' do 5 | # Pods for mobile 6 | pod 'FBLazyVector', :path => "../node_modules/react-native/Libraries/FBLazyVector" 7 | pod 'FBReactNativeSpec', :path => "../node_modules/react-native/Libraries/FBReactNativeSpec" 8 | pod 'RCTRequired', :path => "../node_modules/react-native/Libraries/RCTRequired" 9 | pod 'RCTTypeSafety', :path => "../node_modules/react-native/Libraries/TypeSafety" 10 | pod 'React', :path => '../node_modules/react-native/' 11 | pod 'React-Core', :path => '../node_modules/react-native/' 12 | pod 'React-CoreModules', :path => '../node_modules/react-native/React/CoreModules' 13 | pod 'React-Core/DevSupport', :path => '../node_modules/react-native/' 14 | pod 'React-RCTActionSheet', :path => '../node_modules/react-native/Libraries/ActionSheetIOS' 15 | pod 'React-RCTAnimation', :path => '../node_modules/react-native/Libraries/NativeAnimation' 16 | pod 'React-RCTBlob', :path => '../node_modules/react-native/Libraries/Blob' 17 | pod 'React-RCTImage', :path => '../node_modules/react-native/Libraries/Image' 18 | pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS' 19 | pod 'React-RCTNetwork', :path => '../node_modules/react-native/Libraries/Network' 20 | pod 'React-RCTSettings', :path => '../node_modules/react-native/Libraries/Settings' 21 | pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text' 22 | pod 'React-RCTVibration', :path => '../node_modules/react-native/Libraries/Vibration' 23 | pod 'React-Core/RCTWebSocket', :path => '../node_modules/react-native/' 24 | 25 | pod 'React-cxxreact', :path => '../node_modules/react-native/ReactCommon/cxxreact' 26 | pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi' 27 | pod 'React-jsiexecutor', :path => '../node_modules/react-native/ReactCommon/jsiexecutor' 28 | pod 'React-jsinspector', :path => '../node_modules/react-native/ReactCommon/jsinspector' 29 | pod 'ReactCommon/jscallinvoker', :path => "../node_modules/react-native/ReactCommon" 30 | pod 'ReactCommon/turbomodule/core', :path => "../node_modules/react-native/ReactCommon" 31 | pod 'Yoga', :path => '../node_modules/react-native/ReactCommon/yoga' 32 | 33 | pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec' 34 | pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec' 35 | pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec' 36 | 37 | pod 'BVLinearGradient', :path => '../node_modules/react-native-linear-gradient' 38 | 39 | target 'mobileTests' do 40 | inherit! :search_paths 41 | # Pods for testing 42 | end 43 | 44 | use_native_modules! 45 | end 46 | 47 | target 'mobile-tvOS' do 48 | # Pods for mobile-tvOS 49 | 50 | target 'mobile-tvOSTests' do 51 | inherit! :search_paths 52 | # Pods for testing 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /mobile/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem http://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /mobile/ios/mobile/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /mobile/src/pages/Profile/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | 4 | import Icon from 'react-native-vector-icons/MaterialIcons'; 5 | import { updateProfileRequest } from '~/store/modules/user/actions'; 6 | import { signOut } from '~/store/modules/auth/actions'; 7 | 8 | import Background from '~/components/Background'; 9 | 10 | import { 11 | Container, 12 | Title, 13 | Form, 14 | FormInput, 15 | Separator, 16 | SubmitButton, 17 | LogOutButton, 18 | } from './styles'; 19 | 20 | export default function Profile() { 21 | const profile = useSelector(state => state.user.profile); 22 | 23 | const dispatch = useDispatch(); 24 | const emailRef = useRef(); 25 | const oldPasswordRef = useRef(); 26 | const passwordRef = useRef(); 27 | const confirmPasswordRef = useRef(); 28 | 29 | const [name, setName] = useState(profile.name); 30 | const [email, setEmail] = useState(profile.email); 31 | const [oldPassword, setOldPassword] = useState(''); 32 | const [password, setPassword] = useState(''); 33 | const [confirmPassword, setConfirmPassword] = useState(''); 34 | 35 | useEffect(() => { 36 | setOldPassword(''); 37 | setPassword(''); 38 | setConfirmPassword(''); 39 | }, [profile]); 40 | 41 | function handleSubmit() { 42 | dispatch( 43 | updateProfileRequest({ 44 | name, 45 | email, 46 | oldPassword, 47 | password, 48 | confirmPassword, 49 | }) 50 | ); 51 | } 52 | function handleLogOut() { 53 | dispatch(signOut()); 54 | } 55 | 56 | return ( 57 | 58 | 59 | Meu Perfil 60 |
61 | emailRef.current.focus()} 68 | value={name} 69 | onChangeText={setName} 70 | /> 71 | 72 | oldPassword.current.focus()} 79 | ref={emailRef} 80 | returnKeyType="next" 81 | value={email} 82 | onChangeText={setEmail} 83 | /> 84 | 85 | 86 | passwordRef.current.focus()} 91 | ref={oldPasswordRef} 92 | returnKeyType="next" 93 | value={oldPassword} 94 | onChangeText={setOldPassword} 95 | /> 96 | confirmPasswordRef.current.focus()} 101 | ref={passwordRef} 102 | returnKeyType="next" 103 | value={password} 104 | onChangeText={setPassword} 105 | /> 106 | 116 | Atualizar 117 | Sai do GoBarber 118 | 119 |
120 |
121 | ); 122 | } 123 | 124 | Profile.navigationOptions = { 125 | tabBarLabel: 'Meu perfil', 126 | tabBarIcon: ({ tintColor }) => ( 127 | 128 | ), 129 | }; 130 | -------------------------------------------------------------------------------- /mobile/ios/mobile.xcodeproj/xcshareddata/xcschemes/mobile.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 81 | 82 | 92 | 94 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 113 | 119 | 120 | 121 | 122 | 124 | 125 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /mobile/ios/mobile.xcodeproj/xcshareddata/xcschemes/mobile-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 81 | 82 | 92 | 94 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 113 | 119 | 120 | 121 | 122 | 124 | 125 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /mobile/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin, switch paths to Windows format before running java 129 | if $cygwin ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /mobile/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | 3 | import com.android.build.OutputFile 4 | 5 | /** 6 | * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets 7 | * and bundleReleaseJsAndAssets). 8 | * These basically call `react-native bundle` with the correct arguments during the Android build 9 | * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the 10 | * bundle directly from the development server. Below you can see all the possible configurations 11 | * and their defaults. If you decide to add a configuration block, make sure to add it before the 12 | * `apply from: "../../node_modules/react-native/react.gradle"` line. 13 | * 14 | * project.ext.react = [ 15 | * // the name of the generated asset file containing your JS bundle 16 | * bundleAssetName: "index.android.bundle", 17 | * 18 | * // the entry file for bundle generation 19 | * entryFile: "index.android.js", 20 | * 21 | * // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format 22 | * bundleCommand: "ram-bundle", 23 | * 24 | * // whether to bundle JS and assets in debug mode 25 | * bundleInDebug: false, 26 | * 27 | * // whether to bundle JS and assets in release mode 28 | * bundleInRelease: true, 29 | * 30 | * // whether to bundle JS and assets in another build variant (if configured). 31 | * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants 32 | * // The configuration property can be in the following formats 33 | * // 'bundleIn${productFlavor}${buildType}' 34 | * // 'bundleIn${buildType}' 35 | * // bundleInFreeDebug: true, 36 | * // bundleInPaidRelease: true, 37 | * // bundleInBeta: true, 38 | * 39 | * // whether to disable dev mode in custom build variants (by default only disabled in release) 40 | * // for example: to disable dev mode in the staging build type (if configured) 41 | * devDisabledInStaging: true, 42 | * // The configuration property can be in the following formats 43 | * // 'devDisabledIn${productFlavor}${buildType}' 44 | * // 'devDisabledIn${buildType}' 45 | * 46 | * // the root of your project, i.e. where "package.json" lives 47 | * root: "../../", 48 | * 49 | * // where to put the JS bundle asset in debug mode 50 | * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", 51 | * 52 | * // where to put the JS bundle asset in release mode 53 | * jsBundleDirRelease: "$buildDir/intermediates/assets/release", 54 | * 55 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 56 | * // require('./image.png')), in debug mode 57 | * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", 58 | * 59 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 60 | * // require('./image.png')), in release mode 61 | * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", 62 | * 63 | * // by default the gradle tasks are skipped if none of the JS files or assets change; this means 64 | * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to 65 | * // date; if you have any other folders that you want to ignore for performance reasons (gradle 66 | * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ 67 | * // for example, you might want to remove it from here. 68 | * inputExcludes: ["android/**", "ios/**"], 69 | * 70 | * // override which node gets called and with what additional arguments 71 | * nodeExecutableAndArgs: ["node"], 72 | * 73 | * // supply additional arguments to the packager 74 | * extraPackagerArgs: [] 75 | * ] 76 | */ 77 | 78 | project.ext.react = [ 79 | entryFile: "index.js", 80 | enableHermes: false, // clean and rebuild if changing 81 | ] 82 | 83 | apply from: "../../node_modules/react-native/react.gradle" 84 | 85 | /** 86 | * Set this to true to create two separate APKs instead of one: 87 | * - An APK that only works on ARM devices 88 | * - An APK that only works on x86 devices 89 | * The advantage is the size of the APK is reduced by about 4MB. 90 | * Upload all the APKs to the Play Store and people will download 91 | * the correct one based on the CPU architecture of their device. 92 | */ 93 | def enableSeparateBuildPerCPUArchitecture = false 94 | 95 | /** 96 | * Run Proguard to shrink the Java bytecode in release builds. 97 | */ 98 | def enableProguardInReleaseBuilds = false 99 | 100 | /** 101 | * The preferred build flavor of JavaScriptCore. 102 | * 103 | * For example, to use the international variant, you can use: 104 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` 105 | * 106 | * The international variant includes ICU i18n library and necessary data 107 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that 108 | * give correct results when using with locales other than en-US. Note that 109 | * this variant is about 6MiB larger per architecture than default. 110 | */ 111 | def jscFlavor = 'org.webkit:android-jsc:+' 112 | 113 | /** 114 | * Whether to enable the Hermes VM. 115 | * 116 | * This should be set on project.ext.react and mirrored here. If it is not set 117 | * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode 118 | * and the benefits of using Hermes will therefore be sharply reduced. 119 | */ 120 | def enableHermes = project.ext.react.get("enableHermes", false); 121 | 122 | android { 123 | compileSdkVersion rootProject.ext.compileSdkVersion 124 | 125 | compileOptions { 126 | sourceCompatibility JavaVersion.VERSION_1_8 127 | targetCompatibility JavaVersion.VERSION_1_8 128 | } 129 | 130 | defaultConfig { 131 | applicationId "com.mobile" 132 | minSdkVersion rootProject.ext.minSdkVersion 133 | targetSdkVersion rootProject.ext.targetSdkVersion 134 | versionCode 1 135 | versionName "1.0" 136 | } 137 | splits { 138 | abi { 139 | reset() 140 | enable enableSeparateBuildPerCPUArchitecture 141 | universalApk false // If true, also generate a universal APK 142 | include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" 143 | } 144 | } 145 | signingConfigs { 146 | debug { 147 | storeFile file('debug.keystore') 148 | storePassword 'android' 149 | keyAlias 'androiddebugkey' 150 | keyPassword 'android' 151 | } 152 | } 153 | buildTypes { 154 | debug { 155 | signingConfig signingConfigs.debug 156 | } 157 | release { 158 | // Caution! In production, you need to generate your own keystore file. 159 | // see https://facebook.github.io/react-native/docs/signed-apk-android. 160 | signingConfig signingConfigs.debug 161 | minifyEnabled enableProguardInReleaseBuilds 162 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 163 | } 164 | } 165 | // applicationVariants are e.g. debug, release 166 | applicationVariants.all { variant -> 167 | variant.outputs.each { output -> 168 | // For each separate APK per architecture, set a unique version code as described here: 169 | // https://developer.android.com/studio/build/configure-apk-splits.html 170 | def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] 171 | def abi = output.getFilter(OutputFile.ABI) 172 | if (abi != null) { // null for the universal-debug, universal-release variants 173 | output.versionCodeOverride = 174 | versionCodes.get(abi) * 1048576 + defaultConfig.versionCode 175 | } 176 | 177 | } 178 | } 179 | } 180 | 181 | dependencies { 182 | implementation project(':react-native-linear-gradient') 183 | implementation fileTree(dir: "libs", include: ["*.jar"]) 184 | implementation "com.facebook.react:react-native:+" // From node_modules 185 | implementation 'androidx.appcompat:appcompat:1.1.0-rc01' 186 | implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha02' 187 | 188 | if (enableHermes) { 189 | def hermesPath = "../../node_modules/hermes-engine/android/"; 190 | debugImplementation files(hermesPath + "hermes-debug.aar") 191 | releaseImplementation files(hermesPath + "hermes-release.aar") 192 | } else { 193 | implementation jscFlavor 194 | } 195 | } 196 | 197 | // Run this once to be able to run the application with BUCK 198 | // puts all compile dependencies into folder libs for BUCK to use 199 | task copyDownloadableDepsToLibs(type: Copy) { 200 | from configurations.compile 201 | into 'libs' 202 | } 203 | project.ext.vectoricons = [ 204 | iconFontNames: [ 'MaterialIcons.ttf' ] // Name of the font files you want to copy 205 | ] 206 | 207 | apply from: "../../node_modules/react-native-vector-icons/fonts.gradle" 208 | 209 | apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) 210 | --------------------------------------------------------------------------------