├── server ├── tmp │ ├── .gitkeep │ └── uploads │ │ ├── .gitkeep │ │ ├── 18b9ff3af91f963e4f7a-maria bento 2.jpeg │ │ └── 3ba0d7d8e36412093d3b-Rodrigo Gonalves.jpeg ├── .gitignore ├── .DS_Store ├── src │ ├── .DS_Store │ ├── database │ │ ├── .DS_Store │ │ ├── database.db │ │ ├── index.js │ │ ├── migrations │ │ │ ├── 20220823114449_createHistory.js │ │ │ ├── 20220823161207_createUsersToken.js │ │ │ ├── 20220822190845_createUser.js │ │ │ └── 20220823105515_createExercises.js │ │ └── seeds │ │ │ └── createExercises.js │ ├── configs │ │ ├── auth.js │ │ └── upload.js │ ├── utils │ │ └── AppError.js │ ├── routes │ │ ├── group.routes.js │ │ ├── exercises.routes.js │ │ ├── history.routes.js │ │ ├── sessions.routes.js │ │ ├── index.js │ │ └── users.routes.js │ ├── controllers │ │ ├── GroupsController.js │ │ ├── ExercisesController.js │ │ ├── UserAvatarController.js │ │ ├── SessionsController.js │ │ ├── UserRefreshToken.js │ │ ├── HistoryController.js │ │ └── UsersController.js │ ├── providers │ │ ├── GenerateToken.js │ │ ├── GenerateRefreshToken.js │ │ └── DiskStorage.js │ ├── middlewares │ │ └── ensureAuthenticated.js │ ├── server.js │ └── docs │ │ └── swagger.json ├── exercises │ ├── gif │ │ ├── stiff.gif │ │ ├── abdutora.gif │ │ ├── serrote.gif │ │ ├── barra_cross.gif │ │ ├── corda_cross.gif │ │ ├── neck-press.gif │ │ ├── rosca_punho.gif │ │ ├── martelo_em_pe.gif │ │ ├── pulley_atras.gif │ │ ├── remada_baixa.gif │ │ ├── triceps_testa.gif │ │ ├── crucifixo_reto.gif │ │ ├── pulley_frontal.gif │ │ ├── extensor_de_pernas.gif │ │ ├── leg_press_45_graus.gif │ │ ├── levantamento_terra.gif │ │ ├── rosca_scott_barra_w.gif │ │ ├── supino_reto_com_barra.gif │ │ ├── desenvolvimento_maquina.gif │ │ ├── encolhimento_com_barra.gif │ │ ├── rosca_direta_barra_reta.gif │ │ ├── encolhimento_com_halteres.gif │ │ ├── supino_inclinado_com_barra.gif │ │ ├── frances_deitado_com_halteres.gif │ │ ├── rosca_alternada_com_banco_inclinado.gif │ │ └── elevacao_lateral_com_halteres_sentado.gif │ └── thumb │ │ ├── stiff.png │ │ ├── abdutora.png │ │ ├── serrote.png │ │ ├── barra_cross.png │ │ ├── corda_cross.png │ │ ├── neck-press.png │ │ ├── rosca_punho.png │ │ ├── crucifixo_reto.png │ │ ├── martelo_em_pe.png │ │ ├── pulley_atras.png │ │ ├── pulley_frontal.png │ │ ├── remada_baixa.png │ │ ├── triceps_testa.png │ │ ├── extensor_de_pernas.png │ │ ├── leg_press_45_graus.png │ │ ├── levantamento_terra.png │ │ ├── rosca_scott_barra_w.png │ │ ├── supino_reto_com_barra.png │ │ ├── desenvolvimento_maquina.png │ │ ├── encolhimento_com_barra.png │ │ ├── rosca_direta_barra_reta.png │ │ ├── encolhimento_com_halteres.png │ │ ├── supino_inclinado_com_barra.png │ │ ├── frances_deitado_com_halteres.png │ │ ├── rosca_alternada_com_banco_inclinado.png │ │ └── elevacao_lateral_com_halteres_sentado.png ├── knexfile.js ├── README.md ├── package.json └── insomnia.json ├── mobile ├── src │ ├── @types │ │ ├── png.d.ts │ │ └── svg.d.ts │ ├── assets │ │ ├── background.png │ │ ├── background@2x.png │ │ ├── background@3x.png │ │ ├── userPhotoDefault.png │ │ ├── userPhotoDefault@2x.png │ │ ├── userPhotoDefault@3x.png │ │ ├── home.svg │ │ ├── profile.svg │ │ ├── repetitions.svg │ │ ├── history.svg │ │ ├── series.svg │ │ ├── body.svg │ │ └── logo.svg │ ├── dtos │ │ ├── UserDTO.ts │ │ ├── HistoryDTO.ts │ │ ├── HistoryByDayDTO.ts │ │ └── ExerciseDTO.ts │ ├── utils │ │ └── AppError.ts │ ├── storage │ │ ├── storageConfig.ts │ │ ├── storageUser.ts │ │ └── storageAuthToken.ts │ ├── hooks │ │ └── useAuth.tsx │ ├── components │ │ ├── Loading.tsx │ │ ├── ScreenHeader.tsx │ │ ├── UserPhoto.tsx │ │ ├── Group.tsx │ │ ├── Button.tsx │ │ ├── HistoryCard.tsx │ │ ├── Input.tsx │ │ ├── HomeHeader.tsx │ │ └── ExerciseCard.tsx │ ├── routes │ │ ├── auth.routes.tsx │ │ ├── index.tsx │ │ └── app.routes.tsx │ ├── theme │ │ └── index.ts │ ├── screens │ │ ├── History.tsx │ │ ├── Home.tsx │ │ ├── SignIn.tsx │ │ ├── Exercise.tsx │ │ ├── SignUp.tsx │ │ └── Profile.tsx │ ├── contexts │ │ └── AuthContext.tsx │ └── services │ │ └── api.ts ├── assets │ ├── icon.png │ ├── favicon.png │ ├── splash.png │ └── adaptive-icon.png ├── .prettierrc.json ├── .editorconfig ├── .gitignore ├── metro.config.js ├── tsconfig.json ├── babel.config.js ├── App.tsx ├── app.json └── package.json ├── LICENSE ├── README.md └── README.pt-br.md /server/tmp/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/tmp/uploads/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /mobile/src/@types/png.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' 2 | -------------------------------------------------------------------------------- /server/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/.DS_Store -------------------------------------------------------------------------------- /server/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/src/.DS_Store -------------------------------------------------------------------------------- /mobile/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/mobile/assets/icon.png -------------------------------------------------------------------------------- /mobile/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/mobile/assets/favicon.png -------------------------------------------------------------------------------- /mobile/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/mobile/assets/splash.png -------------------------------------------------------------------------------- /mobile/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/mobile/assets/adaptive-icon.png -------------------------------------------------------------------------------- /server/exercises/gif/stiff.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/stiff.gif -------------------------------------------------------------------------------- /server/src/database/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/src/database/.DS_Store -------------------------------------------------------------------------------- /server/src/database/database.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/src/database/database.db -------------------------------------------------------------------------------- /mobile/src/assets/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/mobile/src/assets/background.png -------------------------------------------------------------------------------- /server/exercises/gif/abdutora.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/abdutora.gif -------------------------------------------------------------------------------- /server/exercises/gif/serrote.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/serrote.gif -------------------------------------------------------------------------------- /server/exercises/thumb/stiff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/stiff.png -------------------------------------------------------------------------------- /mobile/src/assets/background@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/mobile/src/assets/background@2x.png -------------------------------------------------------------------------------- /mobile/src/assets/background@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/mobile/src/assets/background@3x.png -------------------------------------------------------------------------------- /server/exercises/gif/barra_cross.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/barra_cross.gif -------------------------------------------------------------------------------- /server/exercises/gif/corda_cross.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/corda_cross.gif -------------------------------------------------------------------------------- /server/exercises/gif/neck-press.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/neck-press.gif -------------------------------------------------------------------------------- /server/exercises/gif/rosca_punho.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/rosca_punho.gif -------------------------------------------------------------------------------- /server/exercises/thumb/abdutora.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/abdutora.png -------------------------------------------------------------------------------- /server/exercises/thumb/serrote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/serrote.png -------------------------------------------------------------------------------- /server/src/configs/auth.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | jwt: { 3 | secret: "rodrigo", 4 | expiresIn: "1d", 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /mobile/src/assets/userPhotoDefault.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/mobile/src/assets/userPhotoDefault.png -------------------------------------------------------------------------------- /mobile/src/dtos/UserDTO.ts: -------------------------------------------------------------------------------- 1 | export type UserDTO = { 2 | id: string 3 | name: string 4 | email: string 5 | avatar: string 6 | } 7 | -------------------------------------------------------------------------------- /server/exercises/gif/martelo_em_pe.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/martelo_em_pe.gif -------------------------------------------------------------------------------- /server/exercises/gif/pulley_atras.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/pulley_atras.gif -------------------------------------------------------------------------------- /server/exercises/gif/remada_baixa.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/remada_baixa.gif -------------------------------------------------------------------------------- /server/exercises/gif/triceps_testa.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/triceps_testa.gif -------------------------------------------------------------------------------- /server/exercises/thumb/barra_cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/barra_cross.png -------------------------------------------------------------------------------- /server/exercises/thumb/corda_cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/corda_cross.png -------------------------------------------------------------------------------- /server/exercises/thumb/neck-press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/neck-press.png -------------------------------------------------------------------------------- /server/exercises/thumb/rosca_punho.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/rosca_punho.png -------------------------------------------------------------------------------- /mobile/src/assets/userPhotoDefault@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/mobile/src/assets/userPhotoDefault@2x.png -------------------------------------------------------------------------------- /mobile/src/assets/userPhotoDefault@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/mobile/src/assets/userPhotoDefault@3x.png -------------------------------------------------------------------------------- /server/exercises/gif/crucifixo_reto.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/crucifixo_reto.gif -------------------------------------------------------------------------------- /server/exercises/gif/pulley_frontal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/pulley_frontal.gif -------------------------------------------------------------------------------- /server/exercises/thumb/crucifixo_reto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/crucifixo_reto.png -------------------------------------------------------------------------------- /server/exercises/thumb/martelo_em_pe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/martelo_em_pe.png -------------------------------------------------------------------------------- /server/exercises/thumb/pulley_atras.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/pulley_atras.png -------------------------------------------------------------------------------- /server/exercises/thumb/pulley_frontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/pulley_frontal.png -------------------------------------------------------------------------------- /server/exercises/thumb/remada_baixa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/remada_baixa.png -------------------------------------------------------------------------------- /server/exercises/thumb/triceps_testa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/triceps_testa.png -------------------------------------------------------------------------------- /server/exercises/gif/extensor_de_pernas.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/extensor_de_pernas.gif -------------------------------------------------------------------------------- /server/exercises/gif/leg_press_45_graus.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/leg_press_45_graus.gif -------------------------------------------------------------------------------- /server/exercises/gif/levantamento_terra.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/levantamento_terra.gif -------------------------------------------------------------------------------- /server/exercises/gif/rosca_scott_barra_w.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/rosca_scott_barra_w.gif -------------------------------------------------------------------------------- /server/exercises/gif/supino_reto_com_barra.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/supino_reto_com_barra.gif -------------------------------------------------------------------------------- /server/exercises/thumb/extensor_de_pernas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/extensor_de_pernas.png -------------------------------------------------------------------------------- /server/exercises/thumb/leg_press_45_graus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/leg_press_45_graus.png -------------------------------------------------------------------------------- /server/exercises/thumb/levantamento_terra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/levantamento_terra.png -------------------------------------------------------------------------------- /server/exercises/thumb/rosca_scott_barra_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/rosca_scott_barra_w.png -------------------------------------------------------------------------------- /server/exercises/gif/desenvolvimento_maquina.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/desenvolvimento_maquina.gif -------------------------------------------------------------------------------- /server/exercises/gif/encolhimento_com_barra.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/encolhimento_com_barra.gif -------------------------------------------------------------------------------- /server/exercises/gif/rosca_direta_barra_reta.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/rosca_direta_barra_reta.gif -------------------------------------------------------------------------------- /server/exercises/thumb/supino_reto_com_barra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/supino_reto_com_barra.png -------------------------------------------------------------------------------- /server/exercises/gif/encolhimento_com_halteres.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/encolhimento_com_halteres.gif -------------------------------------------------------------------------------- /server/exercises/gif/supino_inclinado_com_barra.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/supino_inclinado_com_barra.gif -------------------------------------------------------------------------------- /server/exercises/thumb/desenvolvimento_maquina.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/desenvolvimento_maquina.png -------------------------------------------------------------------------------- /server/exercises/thumb/encolhimento_com_barra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/encolhimento_com_barra.png -------------------------------------------------------------------------------- /server/exercises/thumb/rosca_direta_barra_reta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/rosca_direta_barra_reta.png -------------------------------------------------------------------------------- /mobile/src/dtos/HistoryDTO.ts: -------------------------------------------------------------------------------- 1 | export type HistoryDTO = { 2 | id: string 3 | name: string 4 | group: string 5 | hour: string 6 | created_at: string 7 | } 8 | -------------------------------------------------------------------------------- /mobile/src/utils/AppError.ts: -------------------------------------------------------------------------------- 1 | export class AppError { 2 | message: string 3 | 4 | constructor(message: string) { 5 | this.message = message 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /server/exercises/gif/frances_deitado_com_halteres.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/frances_deitado_com_halteres.gif -------------------------------------------------------------------------------- /server/exercises/thumb/encolhimento_com_halteres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/encolhimento_com_halteres.png -------------------------------------------------------------------------------- /server/exercises/thumb/supino_inclinado_com_barra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/supino_inclinado_com_barra.png -------------------------------------------------------------------------------- /mobile/src/dtos/HistoryByDayDTO.ts: -------------------------------------------------------------------------------- 1 | import { HistoryDTO } from './HistoryDTO' 2 | 3 | export type HistoryByDayDTO = { 4 | title: string 5 | data: HistoryDTO[] 6 | } 7 | -------------------------------------------------------------------------------- /server/exercises/thumb/frances_deitado_com_halteres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/frances_deitado_com_halteres.png -------------------------------------------------------------------------------- /server/tmp/uploads/18b9ff3af91f963e4f7a-maria bento 2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/tmp/uploads/18b9ff3af91f963e4f7a-maria bento 2.jpeg -------------------------------------------------------------------------------- /mobile/src/storage/storageConfig.ts: -------------------------------------------------------------------------------- 1 | const USER_STORAGE = '@ignitegym:user' 2 | const AUTH_TOKEN_STORAGE = '@ignitegym:token' 3 | 4 | export { USER_STORAGE, AUTH_TOKEN_STORAGE } 5 | -------------------------------------------------------------------------------- /server/exercises/gif/rosca_alternada_com_banco_inclinado.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/rosca_alternada_com_banco_inclinado.gif -------------------------------------------------------------------------------- /server/tmp/uploads/3ba0d7d8e36412093d3b-Rodrigo Gonalves.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/tmp/uploads/3ba0d7d8e36412093d3b-Rodrigo Gonalves.jpeg -------------------------------------------------------------------------------- /server/exercises/gif/elevacao_lateral_com_halteres_sentado.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/gif/elevacao_lateral_com_halteres_sentado.gif -------------------------------------------------------------------------------- /server/exercises/thumb/rosca_alternada_com_banco_inclinado.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/rosca_alternada_com_banco_inclinado.png -------------------------------------------------------------------------------- /mobile/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": false, 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "arrowParens": "avoid", 7 | "endOfLine": "auto" 8 | } 9 | -------------------------------------------------------------------------------- /server/exercises/thumb/elevacao_lateral_com_halteres_sentado.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagnerNerves/ignitegym-rn/HEAD/server/exercises/thumb/elevacao_lateral_com_halteres_sentado.png -------------------------------------------------------------------------------- /server/src/database/index.js: -------------------------------------------------------------------------------- 1 | const config = require("../../knexfile"); 2 | const knex = require("knex"); 3 | 4 | const connection = knex(config.development); 5 | 6 | module.exports = connection; -------------------------------------------------------------------------------- /mobile/src/@types/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import React from 'react' 3 | import { SvgProps } from 'react-native-svg' 4 | const content: React.FC 5 | export default content 6 | } 7 | -------------------------------------------------------------------------------- /mobile/src/dtos/ExerciseDTO.ts: -------------------------------------------------------------------------------- 1 | export type ExerciseDTO = { 2 | id: string 3 | demo: string 4 | group: string 5 | name: string 6 | repetitions: string 7 | series: number 8 | thumb: string 9 | updated_at: string 10 | } 11 | -------------------------------------------------------------------------------- /mobile/src/hooks/useAuth.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | 3 | import { AuthContext } from '@contexts/AuthContext' 4 | 5 | export function useAuth() { 6 | const context = useContext(AuthContext) 7 | 8 | return context 9 | } 10 | -------------------------------------------------------------------------------- /mobile/src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner, Center } from 'native-base' 2 | 3 | export function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /server/src/utils/AppError.js: -------------------------------------------------------------------------------- 1 | class AppError { 2 | message; 3 | statusCode; 4 | 5 | constructor(message, statusCode = 400) { 6 | this.message = message; 7 | this.statusCode = statusCode; 8 | } 9 | } 10 | 11 | module.exports = AppError; -------------------------------------------------------------------------------- /mobile/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | charset = utf-8 10 | indent_style = space 11 | indent_size = 2 12 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /mobile/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | # Temporary files created by Metro to check the health of the file watcher 17 | .metro-health-check* 18 | -------------------------------------------------------------------------------- /server/src/routes/group.routes.js: -------------------------------------------------------------------------------- 1 | const { Router } = require("express"); 2 | 3 | const GroupsController = require("../controllers/GroupsController"); 4 | 5 | const groupRoutes = Router(); 6 | 7 | const groupsController = new GroupsController(); 8 | 9 | groupRoutes.get("/", groupsController.index); 10 | 11 | module.exports = groupRoutes; -------------------------------------------------------------------------------- /mobile/src/components/ScreenHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Center, Heading } from 'native-base' 2 | 3 | type Props = { 4 | title: string 5 | } 6 | 7 | export function ScreenHeader({ title }: Props) { 8 | return ( 9 |
10 | 11 | {title} 12 | 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /mobile/src/components/UserPhoto.tsx: -------------------------------------------------------------------------------- 1 | import { Image, IImageProps } from 'native-base' 2 | 3 | type Props = IImageProps & { 4 | size: number 5 | } 6 | 7 | export function UserPhoto({ size, ...rest }: Props) { 8 | return ( 9 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /server/src/controllers/GroupsController.js: -------------------------------------------------------------------------------- 1 | const knex = require("../database"); 2 | 3 | class GroupsController { 4 | async index(request, response) { 5 | const groups = await knex("exercises").select("group").groupBy("group").orderBy("group"); 6 | 7 | const formattedGroups = groups.map(item => item.group); 8 | 9 | return response.json(formattedGroups); 10 | } 11 | } 12 | 13 | module.exports = GroupsController; -------------------------------------------------------------------------------- /server/src/database/migrations/20220823114449_createHistory.js: -------------------------------------------------------------------------------- 1 | exports.up = knex => knex.schema.createTable("history", table => { 2 | table.increments("id"); 3 | 4 | table.integer("user_id").references("id").inTable("users"); 5 | table.integer("exercise_id").references("id").inTable("exercises"); 6 | 7 | table.timestamp("created_at").default(knex.fn.now()); 8 | }); 9 | 10 | exports.down = knex => knex.schema.dropTable("history"); -------------------------------------------------------------------------------- /server/src/database/migrations/20220823161207_createUsersToken.js: -------------------------------------------------------------------------------- 1 | exports.up = knex => knex.schema.createTable("refresh_token", table => { 2 | table.increments("id"); 3 | table.integer("expires_in") 4 | table.text("refresh_token") 5 | table.integer("user_id").references("id").inTable("users"); 6 | table.timestamp("created_at").default(knex.fn.now()); 7 | }); 8 | 9 | exports.down = knex => knex.schema.dropTable("users_tokens"); 10 | -------------------------------------------------------------------------------- /server/src/providers/GenerateToken.js: -------------------------------------------------------------------------------- 1 | const { sign } = require("jsonwebtoken"); 2 | const authConfig = require("../configs/auth"); 3 | 4 | class GenerateToken { 5 | async execute(userId) { 6 | const { secret, expiresIn } = authConfig.jwt; 7 | 8 | const token = sign({}, secret, { 9 | subject: String(userId), 10 | expiresIn 11 | }); 12 | 13 | return token; 14 | } 15 | } 16 | 17 | module.exports = GenerateToken; -------------------------------------------------------------------------------- /server/src/routes/exercises.routes.js: -------------------------------------------------------------------------------- 1 | const { Router } = require("express"); 2 | 3 | const ExercisesController = require("../controllers/ExercisesController"); 4 | 5 | const exercisesRoutes = Router(); 6 | 7 | const exercisesController = new ExercisesController(); 8 | 9 | exercisesRoutes.get("/bygroup/:group", exercisesController.index); 10 | exercisesRoutes.get("/:id", exercisesController.show); 11 | 12 | module.exports = exercisesRoutes; -------------------------------------------------------------------------------- /server/knexfile.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | development: { 5 | client: "sqlite3", 6 | connection: { 7 | filename: path.resolve(__dirname, "src", "database", "database.db") 8 | }, 9 | migrations: { 10 | directory: path.resolve(__dirname, "src", "database", "migrations") 11 | }, 12 | seeds: { 13 | directory: path.resolve(__dirname, "src", "database", "seeds") 14 | }, 15 | useNullAsDefault: true 16 | } 17 | }; -------------------------------------------------------------------------------- /server/src/database/migrations/20220822190845_createUser.js: -------------------------------------------------------------------------------- 1 | exports.up = knex => knex.schema.createTable("users", table => { 2 | table.increments("id"); 3 | table.text("name").notNullable(); 4 | table.text("email").notNullable(); 5 | table.text("password").notNullable(); 6 | table.text("avatar"); 7 | table.timestamp("created_at").default(knex.fn.now()); 8 | table.timestamp("updated_at").default(knex.fn.now()); 9 | }); 10 | 11 | exports.down = knex => knex.schema.dropTable("users"); -------------------------------------------------------------------------------- /server/src/routes/history.routes.js: -------------------------------------------------------------------------------- 1 | const { Router } = require("express"); 2 | 3 | const HistoryController = require("../controllers/HistoryController"); 4 | const ensureAuthenticated = require("../middlewares/ensureAuthenticated"); 5 | 6 | const historyRoutes = Router(); 7 | 8 | const historyController = new HistoryController(); 9 | 10 | historyRoutes.use(ensureAuthenticated); 11 | 12 | historyRoutes.get("/", historyController.index); 13 | historyRoutes.post("/", historyController.create); 14 | 15 | module.exports = historyRoutes; -------------------------------------------------------------------------------- /server/src/routes/sessions.routes.js: -------------------------------------------------------------------------------- 1 | const { Router } = require("express"); 2 | 3 | const SessionsController = require("../controllers/SessionsController"); 4 | const UserRefreshToken = require("../controllers/UserRefreshToken"); 5 | 6 | const sessionsController = new SessionsController(); 7 | const userRefreshToken = new UserRefreshToken(); 8 | 9 | const sessionsRoutes = Router(); 10 | sessionsRoutes.post("/", sessionsController.create); 11 | sessionsRoutes.post("/refresh-token", userRefreshToken.create); 12 | 13 | module.exports = sessionsRoutes; -------------------------------------------------------------------------------- /mobile/metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require('expo/metro-config') 2 | 3 | module.exports = (() => { 4 | const config = getDefaultConfig(__dirname) 5 | 6 | const { transformer, resolver } = config 7 | 8 | config.transformer = { 9 | ...transformer, 10 | babelTransformerPath: require.resolve('react-native-svg-transformer') 11 | } 12 | config.resolver = { 13 | ...resolver, 14 | assetExts: resolver.assetExts.filter(ext => ext !== 'svg'), 15 | sourceExts: [...resolver.sourceExts, 'svg'] 16 | } 17 | 18 | return config 19 | })() 20 | -------------------------------------------------------------------------------- /server/src/controllers/ExercisesController.js: -------------------------------------------------------------------------------- 1 | const knex = require("../database"); 2 | 3 | class ExercisesController { 4 | async index(request, response) { 5 | const { group } = request.params; 6 | 7 | const exercises = await knex("exercises").where({ group }).orderBy("name"); 8 | 9 | return response.json(exercises); 10 | } 11 | 12 | async show(request, response) { 13 | const { id } = request.params; 14 | 15 | const exercise = await knex("exercises").where({ id }).first(); 16 | 17 | return response.json(exercise); 18 | } 19 | } 20 | 21 | module.exports = ExercisesController; -------------------------------------------------------------------------------- /server/src/database/migrations/20220823105515_createExercises.js: -------------------------------------------------------------------------------- 1 | exports.up = knex => knex.schema.createTable("exercises", table => { 2 | table.increments("id"); 3 | table.text("name").notNullable(); 4 | table.integer("series").notNullable(); 5 | table.integer("repetitions").notNullable(); 6 | table.text("group").notNullable(); 7 | table.text("demo").notNullable(); 8 | table.text("thumb").notNullable(); 9 | table.timestamp("created_at").default(knex.fn.now()); 10 | table.timestamp("updated_at").default(knex.fn.now()); 11 | }); 12 | 13 | exports.down = knex => knex.schema.dropTable("exercises"); -------------------------------------------------------------------------------- /server/src/providers/GenerateRefreshToken.js: -------------------------------------------------------------------------------- 1 | const knex = require("../database"); 2 | const dayjs = require("dayjs"); 3 | const uuid = require('uuid') 4 | 5 | class GenerateRefreshToken { 6 | async execute(user_id) { 7 | await knex("refresh_token").where({ user_id }).delete(); 8 | 9 | const expires_in = dayjs().add(15, "m").unix(); 10 | const refresh_token = uuid.v4(); 11 | 12 | await knex("refresh_token").insert({ 13 | user_id, 14 | expires_in, 15 | refresh_token 16 | }); 17 | 18 | return refresh_token; 19 | } 20 | } 21 | 22 | module.exports = GenerateRefreshToken; -------------------------------------------------------------------------------- /mobile/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "baseUrl": "./", 6 | "paths": { 7 | "@dtos/*": ["./src/dtos/*"], 8 | "@assets/*": ["./src/assets/*"], 9 | "@components/*": ["./src/components/*"], 10 | "@screens/*": ["./src/screens/*"], 11 | "@storage/*": ["./src/storage/*"], 12 | "@utils/*": ["./src/utils/*"], 13 | "@services/*": ["./src/services/*"], 14 | "@hooks/*": ["./src/hooks/*"], 15 | "@contexts/*": ["./src/contexts/*"], 16 | "@routes/*": ["./src/routes/*"] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /server/src/routes/index.js: -------------------------------------------------------------------------------- 1 | const { Router } = require("express"); 2 | 3 | const usersRouter = require("./users.routes"); 4 | const sessionsRouter = require("./sessions.routes"); 5 | const exercisesRouter = require("./exercises.routes"); 6 | const groupRouter = require("./group.routes"); 7 | const historyRouter = require("./history.routes"); 8 | 9 | const routes = Router(); 10 | 11 | routes.use("/users", usersRouter); 12 | routes.use("/sessions", sessionsRouter); 13 | 14 | routes.use("/exercises", exercisesRouter); 15 | routes.use("/groups", groupRouter); 16 | routes.use("/history", historyRouter); 17 | 18 | module.exports = routes; -------------------------------------------------------------------------------- /mobile/src/storage/storageUser.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage' 2 | 3 | import { UserDTO } from '@dtos/UserDTO' 4 | import { USER_STORAGE } from '@storage/storageConfig' 5 | 6 | export async function storageUserSave(user: UserDTO) { 7 | await AsyncStorage.setItem(USER_STORAGE, JSON.stringify(user)) 8 | } 9 | 10 | export async function storageUserGet() { 11 | const storage = await AsyncStorage.getItem(USER_STORAGE) 12 | 13 | const user: UserDTO = storage ? JSON.parse(storage) : {} 14 | 15 | return user 16 | } 17 | 18 | export async function storageUserRemove() { 19 | await AsyncStorage.removeItem(USER_STORAGE) 20 | } 21 | -------------------------------------------------------------------------------- /server/src/configs/upload.js: -------------------------------------------------------------------------------- 1 | const multer = require("multer"); 2 | const crypto = require("crypto"); 3 | const path = require("path"); 4 | 5 | const TMP_FOLDER = path.resolve(__dirname, "..", "..", "tmp"); 6 | const UPLOADS_FOLDER = path.resolve(TMP_FOLDER, "uploads"); 7 | 8 | const MULTER = { 9 | storage: multer.diskStorage({ 10 | destination: TMP_FOLDER, 11 | filename(request, file, callback) { 12 | const fileHash = crypto.randomBytes(10).toString("hex"); 13 | const fileName = `${fileHash}-${file.originalname}`; 14 | 15 | return callback(null, fileName); 16 | }, 17 | }), 18 | }; 19 | 20 | module.exports = { 21 | TMP_FOLDER, 22 | UPLOADS_FOLDER, 23 | MULTER 24 | } -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | 2 | ### Scripts 3 | 4 | | Script | Target | 5 | | ------------------------- | -------------------------------------------------- | 6 | | `npm run dev` | Run API in **development** environment | 7 | | `npm start` | Run API in **production** environment | 8 | | `npm run migrate` | Create database tables | 9 | | `npm run seed` | Populate database tables | 10 | 11 | 12 | ### API Docs 13 | To view the API documentation, run the API and access [http://localhost:3333/api-docs](http://localhost:3333/api-docs) in your browser -------------------------------------------------------------------------------- /server/src/providers/DiskStorage.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const uploadConfig = require("../configs/upload"); 4 | 5 | class DiskStorage { 6 | async saveFile(file) { 7 | 8 | await fs.promises.rename( 9 | path.resolve(uploadConfig.TMP_FOLDER, file), 10 | path.resolve(uploadConfig.UPLOADS_FOLDER, file), 11 | ); 12 | 13 | return file; 14 | } 15 | 16 | async deleteFile(file) { 17 | const filePath = path.resolve(uploadConfig.UPLOADS_FOLDER, file); 18 | 19 | try { 20 | await fs.promises.stat(filePath); 21 | } catch { 22 | return; 23 | } 24 | 25 | await fs.promises.unlink(filePath); 26 | } 27 | } 28 | 29 | module.exports = DiskStorage; -------------------------------------------------------------------------------- /mobile/src/routes/auth.routes.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createNativeStackNavigator, 3 | NativeStackNavigationProp 4 | } from '@react-navigation/native-stack' 5 | 6 | import { SignIn } from '@screens/SignIn' 7 | import { SignUp } from '@screens/SignUp' 8 | 9 | type AuthRoutes = { 10 | signIn: undefined 11 | signUp: undefined 12 | } 13 | 14 | export type AuthNavigatorRoutesProps = NativeStackNavigationProp 15 | 16 | const { Navigator, Screen } = createNativeStackNavigator() 17 | 18 | export function AuthRoutes() { 19 | return ( 20 | 21 | 22 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /mobile/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true) 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: [ 6 | [ 7 | 'module-resolver', 8 | { 9 | root: ['./src'], 10 | alias: { 11 | '@dtos': './src/dtos', 12 | '@assets': './src/assets', 13 | '@components': './src/components', 14 | '@screens': './src/screens', 15 | '@storage': './src/storage', 16 | '@utils': './src/utils', 17 | '@services': './src/services', 18 | '@hooks': './src/hooks', 19 | '@contexts': './src/contexts', 20 | '@routes': './src/routes' 21 | } 22 | } 23 | ] 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /mobile/src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { extendTheme } from 'native-base' 2 | 3 | export const THEME = extendTheme({ 4 | colors: { 5 | green: { 6 | 700: '#00875F', 7 | 500: '#00B37E' 8 | }, 9 | gray: { 10 | 700: '#121214', 11 | 600: '#202024', 12 | 500: '#29292E', 13 | 400: '#323238', 14 | 300: '#7C7C8A', 15 | 200: '#C4C4CC', 16 | 100: '#E1E1E6' 17 | }, 18 | white: '#FFFFFF', 19 | red: { 20 | 500: '#F75A68' 21 | } 22 | }, 23 | fonts: { 24 | heading: 'Roboto_700Bold', 25 | body: 'Roboto_400Regular' 26 | }, 27 | fontSizes: { 28 | xs: 12, 29 | sm: 14, 30 | md: 16, 31 | lg: 18, 32 | xl: 20 33 | }, 34 | sizes: { 35 | 14: 56, 36 | 33: 148 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /server/src/middlewares/ensureAuthenticated.js: -------------------------------------------------------------------------------- 1 | const { verify } = require("jsonwebtoken"); 2 | const AppError = require("../utils/AppError"); 3 | const authConfig = require("../configs/auth"); 4 | 5 | async function ensureAuthenticated(request, response, next) { 6 | const authHeader = request.headers.authorization; 7 | 8 | if (!authHeader) { 9 | throw new AppError("JWT token não informado", 401); 10 | } 11 | 12 | const [, token] = authHeader.split(" "); 13 | 14 | try { 15 | 16 | const { sub: user_id } = verify(token, authConfig.jwt.secret); 17 | 18 | request.user = { 19 | id: Number(user_id), 20 | }; 21 | 22 | return next(); 23 | } catch { 24 | throw new AppError("token.invalid", 401); 25 | } 26 | } 27 | 28 | module.exports = ensureAuthenticated; -------------------------------------------------------------------------------- /mobile/src/components/Group.tsx: -------------------------------------------------------------------------------- 1 | import { Text, Pressable, IPressableProps } from 'native-base' 2 | 3 | type Props = IPressableProps & { 4 | name: string 5 | isActive: boolean 6 | } 7 | 8 | export function Group({ name, isActive, ...rest }: Props) { 9 | return ( 10 | 23 | 29 | {name} 30 | 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /mobile/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme, Box } from 'native-base' 2 | import { NavigationContainer, DefaultTheme } from '@react-navigation/native' 3 | 4 | import { useAuth } from '@hooks/useAuth' 5 | 6 | import { AuthRoutes } from './auth.routes' 7 | import { AppRoutes } from './app.routes' 8 | 9 | import { Loading } from '@components/Loading' 10 | 11 | export function Routes() { 12 | const { colors } = useTheme() 13 | const { user, isLoadingUserStorageData } = useAuth() 14 | 15 | const theme = DefaultTheme 16 | theme.colors.background = colors.gray[700] 17 | 18 | if (isLoadingUserStorageData) { 19 | return 20 | } 21 | 22 | return ( 23 | 24 | 25 | {user.id ? : } 26 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /server/src/routes/users.routes.js: -------------------------------------------------------------------------------- 1 | const { Router } = require("express"); 2 | const multer = require("multer"); 3 | 4 | const uploadConfig = require("../configs/upload"); 5 | const ensureAuthenticated = require("../middlewares/ensureAuthenticated"); 6 | 7 | const UsersController = require("../controllers/UsersController"); 8 | const UserAvatarController = require("../controllers/UserAvatarController"); 9 | 10 | const usersRoutes = Router(); 11 | 12 | const usersController = new UsersController(); 13 | const userAvatarController = new UserAvatarController(); 14 | 15 | const upload = multer(uploadConfig.MULTER); 16 | 17 | usersRoutes.post("/", usersController.create); 18 | usersRoutes.put("/", ensureAuthenticated, usersController.update); 19 | usersRoutes.patch("/avatar", ensureAuthenticated, upload.single("avatar"), userAvatarController.update); 20 | 21 | module.exports = usersRoutes; -------------------------------------------------------------------------------- /mobile/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { Button as ButtonNativeBase, IButtonProps, Text } from 'native-base' 2 | 3 | type Props = IButtonProps & { 4 | title: string 5 | variant?: 'solid' | 'outline' 6 | } 7 | 8 | export function Button({ title, variant = 'solid', ...rest }: Props) { 9 | return ( 10 | 22 | 27 | {title} 28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /mobile/App.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar } from 'react-native' 2 | import { NativeBaseProvider } from 'native-base' 3 | 4 | import { 5 | useFonts, 6 | Roboto_400Regular, 7 | Roboto_700Bold 8 | } from '@expo-google-fonts/roboto' 9 | 10 | import { THEME } from './src/theme' 11 | 12 | import { Loading } from '@components/Loading' 13 | import { Routes } from '@routes/index' 14 | import { AuthContextProvider } from '@contexts/AuthContext' 15 | 16 | export default function App() { 17 | const [fontsLoaded] = useFonts({ Roboto_400Regular, Roboto_700Bold }) 18 | 19 | return ( 20 | 21 | 26 | 27 | 28 | {fontsLoaded ? : } 29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "API desenvolvida para ser utilizada no módulo de consumo de API na trilha de React Native do Ignite.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./src/server.js", 8 | "dev": "nodemon ./src/server.js", 9 | "migrate": "knex migrate:latest", 10 | "seed": "knex seed:run" 11 | }, 12 | "author": "Rodrigo Gonçalves Santana", 13 | "license": "ISC", 14 | "dependencies": { 15 | "bcryptjs": "^2.4.3", 16 | "cors": "^2.8.5", 17 | "dayjs": "^1.11.5", 18 | "express": "^4.18.1", 19 | "express-async-errors": "^3.1.1", 20 | "jsonwebtoken": "^8.5.1", 21 | "knex": "^2.2.0", 22 | "multer": "^1.4.5-lts.1", 23 | "sqlite3": "^5.0.11", 24 | "swagger-ui-express": "^4.5.0", 25 | "uuid": "^9.0.0" 26 | }, 27 | "devDependencies": { 28 | "nodemon": "^2.0.19" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /mobile/src/storage/storageAuthToken.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage' 2 | 3 | import { AUTH_TOKEN_STORAGE } from '@storage/storageConfig' 4 | 5 | type StorageAuthTokenProps = { 6 | token: string 7 | refresh_token: string 8 | } 9 | 10 | export async function storageAuthTokenSave({ 11 | token, 12 | refresh_token 13 | }: StorageAuthTokenProps) { 14 | await AsyncStorage.setItem( 15 | AUTH_TOKEN_STORAGE, 16 | JSON.stringify({ token, refresh_token }) 17 | ) 18 | } 19 | 20 | export async function storageAuthTokenGet() { 21 | const response = await AsyncStorage.getItem(AUTH_TOKEN_STORAGE) 22 | 23 | const { token, refresh_token }: StorageAuthTokenProps = response 24 | ? JSON.parse(response) 25 | : {} 26 | 27 | return { token, refresh_token } 28 | } 29 | 30 | export async function storageAuthTokenRemove() { 31 | await AsyncStorage.removeItem(AUTH_TOKEN_STORAGE) 32 | } 33 | -------------------------------------------------------------------------------- /server/src/controllers/UserAvatarController.js: -------------------------------------------------------------------------------- 1 | const knex = require("../database"); 2 | const DiskStorage = require("../providers/DiskStorage"); 3 | 4 | class UserAvatarController { 5 | async update(request, response) { 6 | const user_id = request.user.id; 7 | const avatarFilename = request.file.filename; 8 | 9 | const diskStorage = new DiskStorage(); 10 | 11 | const user = await knex("users").where({ id: user_id }).first(); 12 | 13 | if (!user) { 14 | throw new AppError("Somente usuários autenticados podem mudar o avatar", 401); 15 | } 16 | 17 | if (user.avatar) { 18 | await diskStorage.deleteFile(user.avatar); 19 | } 20 | 21 | const filename = await diskStorage.saveFile(avatarFilename); 22 | user.avatar = filename; 23 | 24 | await knex("users").where({ id: user_id }).update(user); 25 | 26 | return response.json(user); 27 | } 28 | } 29 | 30 | module.exports = UserAvatarController; -------------------------------------------------------------------------------- /mobile/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "ignitegym-rn", 4 | "slug": "ignitegym-rn", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "cover", 12 | "backgroundColor": "#121214" 13 | }, 14 | "plugins": [ 15 | [ 16 | "expo-image-picker", 17 | { 18 | "photosPermission": "The app accesses your photos to let you share them with your friends." 19 | } 20 | ] 21 | ], 22 | "assetBundlePatterns": ["**/*"], 23 | "ios": { 24 | "supportsTablet": true 25 | }, 26 | "android": { 27 | "adaptiveIcon": { 28 | "foregroundImage": "./assets/adaptive-icon.png", 29 | "backgroundColor": "#ffffff" 30 | } 31 | }, 32 | "web": { 33 | "favicon": "./assets/favicon.png" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /mobile/src/components/HistoryCard.tsx: -------------------------------------------------------------------------------- 1 | import { HStack, Heading, Text, VStack } from 'native-base' 2 | 3 | import { HistoryDTO } from '@dtos/HistoryDTO' 4 | 5 | type Props = { 6 | data: HistoryDTO 7 | } 8 | 9 | export function HistoryCard({ data }: Props) { 10 | return ( 11 | 21 | 22 | 29 | {data.group} 30 | 31 | 32 | 33 | {data.name} 34 | 35 | 36 | 37 | 38 | {data.hour} 39 | 40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /mobile/src/components/Input.tsx: -------------------------------------------------------------------------------- 1 | import { Input as NativeBaseInput, IInputProps, FormControl } from 'native-base' 2 | 3 | type Props = IInputProps & { 4 | errorMessage?: string | null 5 | } 6 | 7 | export function Input({ errorMessage = null, isInvalid, ...rest }: Props) { 8 | const invalid = !!errorMessage || isInvalid 9 | 10 | return ( 11 | 12 | 33 | 34 | 35 | {errorMessage} 36 | 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Vagner Nerves 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mobile/src/assets/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /mobile/src/assets/profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /server/src/controllers/SessionsController.js: -------------------------------------------------------------------------------- 1 | const knex = require("../database"); 2 | const { compare } = require("bcryptjs"); 3 | const AppError = require("../utils/AppError"); 4 | const GenerateRefreshToken = require("../providers/GenerateRefreshToken"); 5 | const GenerateToken = require("../providers/GenerateToken"); 6 | 7 | class SessionsController { 8 | async create(request, response) { 9 | const { email, password } = request.body; 10 | 11 | const user = await knex("users").where({ email: email.toLowerCase() }).first(); 12 | 13 | if (!user) { 14 | throw new AppError("E-mail e/ou senha incorreta.", 404); 15 | } 16 | 17 | const passwordMatched = await compare(password, user.password); 18 | 19 | if (!passwordMatched) { 20 | throw new AppError("E-mail e/ou senha incorreta.", 404); 21 | } 22 | 23 | const generateTokenProvider = new GenerateToken(); 24 | const token = await generateTokenProvider.execute(user.id); 25 | 26 | const generateRefreshToken = new GenerateRefreshToken(); 27 | const refresh_token = await generateRefreshToken.execute(user.id); 28 | 29 | delete user.password; 30 | 31 | response.status(201).json({ user, token, refresh_token }); 32 | } 33 | } 34 | 35 | module.exports = SessionsController; -------------------------------------------------------------------------------- /mobile/src/components/HomeHeader.tsx: -------------------------------------------------------------------------------- 1 | import { TouchableOpacity } from 'react-native' 2 | import { MaterialIcons } from '@expo/vector-icons' 3 | import { HStack, Heading, Text, VStack, Icon } from 'native-base' 4 | 5 | import { api } from '@services/api' 6 | import { useAuth } from '@hooks/useAuth' 7 | 8 | import defaultUserPhotoImg from '@assets/userPhotoDefault.png' 9 | 10 | import { UserPhoto } from './UserPhoto' 11 | 12 | export function HomeHeader() { 13 | const { user, signOut } = useAuth() 14 | 15 | return ( 16 | 17 | 27 | 28 | 29 | Olá, 30 | 31 | 32 | 33 | {user.name} 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /mobile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ignitegym-rn", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "@expo-google-fonts/roboto": "^0.2.3", 13 | "@hookform/resolvers": "^3.1.1", 14 | "@react-navigation/bottom-tabs": "^6.5.7", 15 | "@react-navigation/native": "^6.1.6", 16 | "@react-navigation/native-stack": "^6.9.12", 17 | "axios": "^1.4.0", 18 | "expo": "~48.0.18", 19 | "expo-file-system": "~15.2.2", 20 | "expo-font": "~11.1.1", 21 | "expo-image-picker": "~14.1.1", 22 | "expo-status-bar": "~1.4.4", 23 | "native-base": "^3.4.28", 24 | "react": "18.2.0", 25 | "react-hook-form": "^7.45.0", 26 | "react-native": "0.71.8", 27 | "react-native-safe-area-context": "4.5.0", 28 | "react-native-screens": "~3.20.0", 29 | "react-native-svg": "13.4.0", 30 | "yup": "^1.2.0", 31 | "@react-native-async-storage/async-storage": "1.17.11" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.20.0", 35 | "@types/react": "~18.0.14", 36 | "babel-plugin-module-resolver": "^5.0.0", 37 | "prettier": "2.8.8", 38 | "react-native-svg-transformer": "^1.0.0", 39 | "typescript": "^4.9.4" 40 | }, 41 | "private": true 42 | } 43 | -------------------------------------------------------------------------------- /server/src/server.js: -------------------------------------------------------------------------------- 1 | require("express-async-errors"); 2 | 3 | const path = require("path"); 4 | 5 | const swaggerDocument = require("./docs/swagger.json"); 6 | const swaggerUI = require("swagger-ui-express"); 7 | const uploadConfig = require("./configs/upload"); 8 | const AppError = require("./utils/AppError"); 9 | const express = require("express"); 10 | const cors = require("cors"); 11 | 12 | const app = express(); 13 | 14 | app.use("/avatar", express.static(uploadConfig.UPLOADS_FOLDER)); 15 | 16 | const demoExercisePath = path.resolve(__dirname, "..", "exercises", "gif") 17 | app.use("/exercise/demo", express.static(demoExercisePath)); 18 | 19 | const thumbExercisesPath = path.resolve(__dirname, "..", "exercises", "thumb") 20 | app.use("/exercise/thumb", express.static(thumbExercisesPath)); 21 | 22 | 23 | const routes = require("./routes"); 24 | 25 | app.use(express.json()); 26 | app.use(cors()); 27 | 28 | app.use('/api-docs', swaggerUI.serve, swaggerUI.setup(swaggerDocument)); 29 | 30 | app.use(routes); 31 | 32 | app.use((err, request, response, next) => { 33 | if (err instanceof AppError) { 34 | return response.status(err.statusCode).json({ 35 | status: "error", 36 | message: err.message, 37 | }); 38 | } 39 | 40 | console.error(err); 41 | 42 | return response.status(500).json({ 43 | status: "error", 44 | message: "Internal server error", 45 | }); 46 | }); 47 | 48 | const PORT = 3333; 49 | app.listen(PORT, () => console.log(`Server is running on Port ${PORT}`)); -------------------------------------------------------------------------------- /mobile/src/components/ExerciseCard.tsx: -------------------------------------------------------------------------------- 1 | import { TouchableOpacity, TouchableOpacityProps } from 'react-native' 2 | import { HStack, Heading, Image, Text, VStack, Icon } from 'native-base' 3 | import { Entypo } from '@expo/vector-icons' 4 | 5 | import { api } from '@services/api' 6 | 7 | import { ExerciseDTO } from '@dtos/ExerciseDTO' 8 | 9 | type Props = TouchableOpacityProps & { 10 | data: ExerciseDTO 11 | } 12 | 13 | export function ExerciseCard({ data, ...rest }: Props) { 14 | return ( 15 | 16 | 24 | Imagem do exercício 35 | 36 | 37 | 38 | {data.name} 39 | 40 | 41 | 42 | {data.series} séries x {data.repetitions} repetições 43 | 44 | 45 | 46 | 47 | 48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /server/src/controllers/UserRefreshToken.js: -------------------------------------------------------------------------------- 1 | const knex = require("../database"); 2 | const AppError = require("../utils/AppError"); 3 | const GenerateRefreshToken = require("../providers/GenerateRefreshToken"); 4 | const GenerateToken = require("../providers/GenerateToken"); 5 | const dayjs = require("dayjs"); 6 | 7 | class UserRefreshToken { 8 | async create(request, response) { 9 | const { refresh_token } = request.body; 10 | 11 | if (!refresh_token) { 12 | throw new AppError("Informe o token de autenticação.", 401); 13 | } 14 | 15 | const refreshToken = await knex("refresh_token").where({ refresh_token }).first(); 16 | 17 | if (!refreshToken) { 18 | throw new AppError("Refresh token não encontrado para este usuário.", 401); 19 | } 20 | 21 | const generateTokenProvider = new GenerateToken(); 22 | const token = await generateTokenProvider.execute(refreshToken.user_id); 23 | 24 | const refreshTokenExpired = dayjs().isAfter(dayjs.unix(refreshToken.expires_in)); 25 | 26 | if (refreshTokenExpired) { 27 | await knex("refresh_token").where({ user_id: refreshToken.user_id }).delete(); 28 | 29 | const generateRefreshToken = new GenerateRefreshToken(); 30 | const newRefreshToken = await generateRefreshToken.execute(refreshToken.user_id, refresh_token); 31 | 32 | return response.json({ token, refresh_token: newRefreshToken }); 33 | } 34 | 35 | return response.json({ token, refresh_token }); 36 | } 37 | } 38 | 39 | module.exports = UserRefreshToken; -------------------------------------------------------------------------------- /mobile/src/assets/repetitions.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /server/src/controllers/HistoryController.js: -------------------------------------------------------------------------------- 1 | const AppError = require("../utils/AppError"); 2 | const knex = require("../database"); 3 | const dayjs = require("dayjs"); 4 | 5 | class HistoryController { 6 | async index(request, response) { 7 | const user_id = request.user.id; 8 | 9 | const history = await knex("history") 10 | .select( 11 | "history.id", 12 | "history.user_id", 13 | "history.exercise_id", 14 | "exercises.name", 15 | "exercises.group", 16 | "history.created_at" 17 | ) 18 | .leftJoin("exercises", "exercises.id", "=", "history.exercise_id") 19 | .where({ user_id }).orderBy("history.created_at", "desc"); 20 | 21 | const days = []; 22 | 23 | for (let exercise of history) { 24 | const day = dayjs(exercise.created_at).format('DD.MM.YYYY'); 25 | 26 | if (!days.includes(day)) { 27 | days.push(day); 28 | } 29 | } 30 | 31 | const exercisesByDay = days.map(day => { 32 | const exercises = history 33 | .filter((exercise) => dayjs(exercise.created_at).format('DD.MM.YYYY') === day). 34 | map((exercise) => { 35 | return { 36 | ...exercise, 37 | hour: dayjs(exercise.created_at).format('HH:mm') 38 | } 39 | }); 40 | 41 | return ({ title: day, data: exercises }); 42 | }); 43 | 44 | 45 | return response.json(exercisesByDay); 46 | } 47 | 48 | async create(request, response) { 49 | const { exercise_id } = request.body; 50 | const user_id = request.user.id; 51 | 52 | if (!exercise_id) { 53 | throw new AppError("Informe o id do exercício."); 54 | } 55 | 56 | await knex("history").insert({ user_id, exercise_id }); 57 | 58 | return response.status(201).json(); 59 | } 60 | } 61 | 62 | module.exports = HistoryController; -------------------------------------------------------------------------------- /mobile/src/assets/history.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /server/src/controllers/UsersController.js: -------------------------------------------------------------------------------- 1 | const knex = require("../database"); 2 | const { hash, compare } = require("bcryptjs"); 3 | const AppError = require("../utils/AppError"); 4 | 5 | class UsersController { 6 | async create(request, response) { 7 | const { name, email, password } = request.body; 8 | 9 | if (!name || !email || !password) { 10 | throw new AppError("Informe todos os campos (nome, email e senha)."); 11 | } 12 | 13 | const checkUserExists = await knex("users").where({ email }).first(); 14 | 15 | if (checkUserExists) { 16 | throw new AppError("Este e-mail já está em uso."); 17 | } 18 | 19 | const hashedPassword = await hash(password, 8); 20 | 21 | await knex("users").insert({ 22 | name, 23 | email, 24 | password: hashedPassword 25 | }); 26 | 27 | return response.status(201).json(); 28 | } 29 | 30 | async update(request, response) { 31 | const { name, password, old_password } = request.body; 32 | const user_id = request.user.id; 33 | 34 | const user = await knex("users").where({ id: user_id }).first(); 35 | 36 | if (!user) { 37 | throw new AppError("Usuário não encontrado", 404); 38 | } 39 | 40 | user.name = name ?? user.name; 41 | 42 | if (password && !old_password) { 43 | throw new AppError( 44 | "Você precisa informar a senha antiga para definir a nova senha.", 45 | ); 46 | } 47 | 48 | 49 | if (!password && old_password) { 50 | throw new AppError( 51 | "Informe a nova senha.", 52 | ); 53 | } 54 | 55 | if (password && old_password) { 56 | const checkOldPassword = await compare(old_password, user.password); 57 | 58 | if (!checkOldPassword) { 59 | throw new AppError("A senha antiga não confere."); 60 | } 61 | 62 | user.password = await hash(password, 8); 63 | } 64 | 65 | await knex("users").where({ id: user_id }).update(user); 66 | 67 | return response.json(); 68 | } 69 | } 70 | 71 | module.exports = UsersController; -------------------------------------------------------------------------------- /mobile/src/assets/series.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /mobile/src/routes/app.routes.tsx: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native' 2 | import { useTheme } from 'native-base' 3 | import { 4 | createBottomTabNavigator, 5 | BottomTabNavigationProp 6 | } from '@react-navigation/bottom-tabs' 7 | 8 | import HomeSvg from '@assets/home.svg' 9 | import HistorySvg from '@assets/history.svg' 10 | import ProfileSvg from '@assets/profile.svg' 11 | 12 | import { Home } from '@screens/Home' 13 | import { History } from '@screens/History' 14 | import { Profile } from '@screens/Profile' 15 | import { Exercise } from '@screens/Exercise' 16 | 17 | type AppRoutes = { 18 | home: undefined 19 | history: undefined 20 | profile: undefined 21 | exercise: { 22 | exerciseId: string 23 | } 24 | } 25 | 26 | export type AppNavigatorRoutesProps = BottomTabNavigationProp 27 | 28 | const { Navigator, Screen } = createBottomTabNavigator() 29 | 30 | export function AppRoutes() { 31 | const { sizes, colors } = useTheme() 32 | 33 | const iconSize = sizes[6] 34 | 35 | return ( 36 | 51 | ( 56 | 57 | ) 58 | }} 59 | /> 60 | ( 65 | 66 | ) 67 | }} 68 | /> 69 | ( 74 | 75 | ) 76 | }} 77 | /> 78 | null }} 82 | /> 83 | 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /mobile/src/screens/History.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | import { useFocusEffect } from '@react-navigation/native' 3 | import { Heading, VStack, SectionList, Text, useToast } from 'native-base' 4 | 5 | import { api } from '@services/api' 6 | import { AppError } from '@utils/AppError' 7 | import { HistoryByDayDTO } from '@dtos/HistoryByDayDTO' 8 | 9 | import { ScreenHeader } from '@components/ScreenHeader' 10 | import { HistoryCard } from '@components/HistoryCard' 11 | import { Loading } from '@components/Loading' 12 | 13 | export function History() { 14 | const [isLoading, setIsLoading] = useState(true) 15 | const [exercises, setExercises] = useState([]) 16 | 17 | const toast = useToast() 18 | 19 | async function fetchHistory() { 20 | try { 21 | setIsLoading(true) 22 | 23 | const response = await api.get('/history') 24 | setExercises(response.data) 25 | } catch (error) { 26 | const isAppError = error instanceof AppError 27 | const title = isAppError 28 | ? error.message 29 | : 'Não foi possível carregar o histórico.' 30 | 31 | toast.show({ 32 | title, 33 | placement: 'top', 34 | bgColor: 'red.500' 35 | }) 36 | } finally { 37 | setIsLoading(false) 38 | } 39 | } 40 | 41 | useFocusEffect( 42 | useCallback(() => { 43 | fetchHistory() 44 | }, []) 45 | ) 46 | 47 | return ( 48 | 49 | 50 | 51 | {isLoading ? ( 52 | 53 | ) : ( 54 | item.id} 57 | renderItem={({ item }) => } 58 | renderSectionHeader={({ section }) => ( 59 | 66 | {section.title} 67 | 68 | )} 69 | px={8} 70 | contentContainerStyle={ 71 | exercises.length === 0 && { 72 | flex: 1, 73 | justifyContent: 'center' 74 | } 75 | } 76 | ListEmptyComponent={() => ( 77 | 78 | Não há exercícios registrados ainda. {'\n'} 79 | Vamos fazer exercícios hoje? 80 | 81 | )} 82 | showsVerticalScrollIndicator={false} 83 | stickySectionHeadersEnabled={false} 84 | /> 85 | )} 86 | 87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /mobile/src/contexts/AuthContext.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, createContext, useEffect, useState } from 'react' 2 | 3 | import { 4 | storageAuthTokenSave, 5 | storageAuthTokenGet, 6 | storageAuthTokenRemove 7 | } from '@storage/storageAuthToken' 8 | import { 9 | storageUserSave, 10 | storageUserGet, 11 | storageUserRemove 12 | } from '@storage/storageUser' 13 | 14 | import { api } from '@services/api' 15 | 16 | import { UserDTO } from '@dtos/UserDTO' 17 | 18 | export type AuthContextDataProps = { 19 | user: UserDTO 20 | updateUserProfile: (userUpdated: UserDTO) => Promise 21 | signIn: (email: string, password: string) => Promise 22 | signOut: () => Promise 23 | isLoadingUserStorageData: boolean 24 | } 25 | 26 | type AuthContextProviderProps = { 27 | children: ReactNode 28 | } 29 | 30 | export const AuthContext = createContext( 31 | {} as AuthContextDataProps 32 | ) 33 | 34 | export function AuthContextProvider({ children }: AuthContextProviderProps) { 35 | const [user, setUser] = useState({} as UserDTO) 36 | const [isLoadingUserStorageData, setIsLoadingUserStorageData] = useState(true) 37 | 38 | async function userAndTokenUpdate(userData: UserDTO, token: string) { 39 | api.defaults.headers.common['Authorization'] = `Bearer ${token}` 40 | setUser(userData) 41 | } 42 | 43 | async function storageUserAndTokenSave( 44 | userData: UserDTO, 45 | token: string, 46 | refresh_token: string 47 | ) { 48 | try { 49 | setIsLoadingUserStorageData(true) 50 | 51 | await storageUserSave(userData) 52 | await storageAuthTokenSave({ token, refresh_token }) 53 | } catch (error) { 54 | throw error 55 | } finally { 56 | setIsLoadingUserStorageData(false) 57 | } 58 | } 59 | 60 | async function signIn(email: string, password: string) { 61 | try { 62 | const { data } = await api.post('/sessions', { email, password }) 63 | 64 | if (data.user && data.token && data.refresh_token) { 65 | await storageUserAndTokenSave(data.user, data.token, data.refresh_token) 66 | 67 | userAndTokenUpdate(data.user, data.token) 68 | } 69 | } catch (error) { 70 | throw error 71 | } finally { 72 | setIsLoadingUserStorageData(false) 73 | } 74 | } 75 | 76 | async function signOut() { 77 | try { 78 | setIsLoadingUserStorageData(true) 79 | 80 | setUser({} as UserDTO) 81 | await storageUserRemove() 82 | await storageAuthTokenRemove() 83 | } catch (error) { 84 | throw error 85 | } finally { 86 | setIsLoadingUserStorageData(false) 87 | } 88 | } 89 | 90 | async function updateUserProfile(userUpdate: UserDTO) { 91 | try { 92 | setUser(userUpdate) 93 | await storageUserSave(userUpdate) 94 | } catch (error) { 95 | throw error 96 | } 97 | } 98 | 99 | async function loadUserData() { 100 | try { 101 | setIsLoadingUserStorageData(true) 102 | 103 | const userLogged = await storageUserGet() 104 | const { token } = await storageAuthTokenGet() 105 | 106 | if (token && userLogged) { 107 | userAndTokenUpdate(userLogged, token) 108 | } 109 | } catch (error) { 110 | throw error 111 | } finally { 112 | setIsLoadingUserStorageData(false) 113 | } 114 | } 115 | 116 | useEffect(() => { 117 | loadUserData() 118 | }, []) 119 | 120 | useEffect(() => { 121 | const subscribe = api.registerInterceptTokenManager(signOut) 122 | 123 | return () => { 124 | subscribe() 125 | } 126 | }, [signOut]) 127 | 128 | return ( 129 | 138 | {children} 139 | 140 | ) 141 | } 142 | -------------------------------------------------------------------------------- /mobile/src/assets/body.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /mobile/src/services/api.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance, AxiosError } from 'axios' 2 | 3 | import { AppError } from '@utils/AppError' 4 | import { 5 | storageAuthTokenGet, 6 | storageAuthTokenSave 7 | } from '@storage/storageAuthToken' 8 | 9 | type SignOut = () => void 10 | 11 | type PromiseType = { 12 | onSucess: (token: string) => void 13 | onFailure: (error: AxiosError) => void 14 | } 15 | 16 | type APIInstanceProps = AxiosInstance & { 17 | registerInterceptTokenManager: (signOut: SignOut) => () => void 18 | } 19 | 20 | const api = axios.create({ 21 | baseURL: 'http://192.168.1.6:3333', 22 | timeout: 6000 23 | }) as APIInstanceProps 24 | 25 | let failedQueue: Array = [] 26 | let isRefreshing = false 27 | 28 | api.registerInterceptTokenManager = signOut => { 29 | const interceptTokenManager = api.interceptors.response.use( 30 | response => response, 31 | async requestError => { 32 | if (requestError?.response?.status === 401) { 33 | if ( 34 | requestError.response.data?.message === 'token.expired' || 35 | requestError.response.data?.message === 'token.invalid' 36 | ) { 37 | const { refresh_token } = await storageAuthTokenGet() 38 | 39 | if (!refresh_token) { 40 | signOut() 41 | return Promise.reject(requestError) 42 | } 43 | 44 | const originalRequestConfig = requestError.config 45 | 46 | if (isRefreshing) { 47 | return new Promise((resolve, reject) => { 48 | failedQueue.push({ 49 | onSucess: (token: string) => { 50 | originalRequestConfig.headers = { 51 | ...originalRequestConfig.headers, 52 | Authorization: `Bearer ${token}` 53 | } 54 | resolve(api(originalRequestConfig)) 55 | }, 56 | onFailure: (error: AxiosError) => { 57 | reject(error) 58 | } 59 | }) 60 | }) 61 | } 62 | 63 | isRefreshing = true 64 | 65 | return new Promise(async (resolve, reject) => { 66 | try { 67 | const { data } = await api.post('/sessions/refresh-token', { 68 | refresh_token 69 | }) 70 | 71 | await storageAuthTokenSave({ 72 | token: data.token, 73 | refresh_token: data.refresh_token 74 | }) 75 | 76 | if ( 77 | originalRequestConfig.data && 78 | !(originalRequestConfig.data instanceof FormData) 79 | ) { 80 | originalRequestConfig.data = JSON.parse( 81 | originalRequestConfig.data 82 | ) 83 | } 84 | 85 | originalRequestConfig.headers = { 86 | ...originalRequestConfig.headers, 87 | Authorization: `Bearer ${data.token}` 88 | } 89 | 90 | api.defaults.headers.common[ 91 | 'Authorization' 92 | ] = `Bearer ${data.token}` 93 | 94 | failedQueue.forEach(request => { 95 | request.onSucess(data.token) 96 | }) 97 | 98 | resolve(api(originalRequestConfig)) 99 | } catch (error: any) { 100 | failedQueue.forEach(request => { 101 | request.onFailure(error) 102 | }) 103 | 104 | signOut() 105 | reject(error) 106 | } finally { 107 | isRefreshing = false 108 | failedQueue = [] 109 | } 110 | }) 111 | } 112 | 113 | signOut() 114 | } 115 | 116 | if (requestError.response && requestError.response.data) { 117 | return Promise.reject(new AppError(requestError.response.data.message)) 118 | } else { 119 | return Promise.reject(requestError) 120 | } 121 | } 122 | ) 123 | 124 | return () => { 125 | api.interceptors.response.eject(interceptTokenManager) 126 | } 127 | } 128 | 129 | export { api } 130 | -------------------------------------------------------------------------------- /mobile/src/screens/Home.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react' 2 | import { useNavigation, useFocusEffect } from '@react-navigation/native' 3 | import { VStack, FlatList, HStack, Heading, Text, useToast } from 'native-base' 4 | 5 | import { api } from '@services/api' 6 | import { AppError } from '@utils/AppError' 7 | import { ExerciseDTO } from '@dtos/ExerciseDTO' 8 | 9 | import { AppNavigatorRoutesProps } from '@routes/app.routes' 10 | 11 | import { Group } from '@components/Group' 12 | import { HomeHeader } from '@components/HomeHeader' 13 | import { ExerciseCard } from '@components/ExerciseCard' 14 | import { Loading } from '@components/Loading' 15 | 16 | export function Home() { 17 | const [isLoading, setIsLoading] = useState(true) 18 | const [groups, setGrous] = useState([]) 19 | const [exercises, setExercises] = useState([]) 20 | const [groupSelected, setGroupSetected] = useState('') 21 | 22 | const toast = useToast() 23 | const navigation = useNavigation() 24 | 25 | function handleOpenExerciseDetails(exerciseId: string) { 26 | navigation.navigate('exercise', { exerciseId }) 27 | } 28 | 29 | async function fetchGroups() { 30 | try { 31 | const responde = await api.get('/groups') 32 | 33 | setGrous(responde.data) 34 | setGroupSetected(responde.data[0]) 35 | } catch (error) { 36 | const isAppError = error instanceof AppError 37 | const title = isAppError 38 | ? error.message 39 | : 'Não foi possível carregar os grupos musculares.' 40 | 41 | toast.show({ 42 | title, 43 | placement: 'top', 44 | bgColor: 'red.500' 45 | }) 46 | } 47 | } 48 | 49 | async function fetchExercisesByGroup() { 50 | try { 51 | setIsLoading(true) 52 | const response = await api.get(`/exercises/bygroup/${groupSelected}`) 53 | 54 | setExercises(response.data) 55 | } catch (error) { 56 | const isAppError = error instanceof AppError 57 | const title = isAppError 58 | ? error.message 59 | : 'Não foi possível carregar os exercicios.' 60 | 61 | toast.show({ 62 | title, 63 | placement: 'top', 64 | bgColor: 'red.500' 65 | }) 66 | } finally { 67 | setIsLoading(false) 68 | } 69 | } 70 | 71 | useEffect(() => { 72 | fetchGroups() 73 | }, []) 74 | 75 | useFocusEffect( 76 | useCallback(() => { 77 | fetchExercisesByGroup() 78 | }, [groupSelected]) 79 | ) 80 | 81 | return ( 82 | 83 | 84 | 85 | item} 88 | renderItem={({ item }) => ( 89 | setGroupSetected(item)} 93 | /> 94 | )} 95 | horizontal 96 | showsHorizontalScrollIndicator={false} 97 | _contentContainerStyle={{ px: 8 }} 98 | my={10} 99 | maxH={10} 100 | minH={10} 101 | /> 102 | 103 | {isLoading ? ( 104 | 105 | ) : ( 106 | 107 | 108 | 109 | Exercícios 110 | 111 | 112 | 113 | {exercises.length} 114 | 115 | 116 | 117 | item.id} 120 | renderItem={({ item }) => ( 121 | handleOpenExerciseDetails(item.id)} 123 | data={item} 124 | /> 125 | )} 126 | showsVerticalScrollIndicator={false} 127 | _contentContainerStyle={{ paddingBottom: 20 }} 128 | /> 129 | 130 | )} 131 | 132 | ) 133 | } 134 | -------------------------------------------------------------------------------- /mobile/src/screens/SignIn.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Platform } from 'react-native' 3 | import { useNavigation } from '@react-navigation/native' 4 | import { useForm, Controller } from 'react-hook-form' 5 | import { yupResolver } from '@hookform/resolvers/yup' 6 | import * as yup from 'yup' 7 | 8 | import { AuthNavigatorRoutesProps } from '@routes/auth.routes' 9 | 10 | import { useAuth } from '@hooks/useAuth' 11 | 12 | import { 13 | VStack, 14 | Image, 15 | Text, 16 | Center, 17 | Heading, 18 | ScrollView, 19 | KeyboardAvoidingView, 20 | useToast 21 | } from 'native-base' 22 | 23 | import { AppError } from '@utils/AppError' 24 | 25 | import LogoSvg from '@assets/logo.svg' 26 | import BackgroundImg from '@assets/background.png' 27 | 28 | import { Input } from '@components/Input' 29 | import { Button } from '@components/Button' 30 | 31 | type FormDataProps = { 32 | email: string 33 | password: string 34 | } 35 | 36 | const signInSchema = yup.object({ 37 | email: yup.string().required('Informe o e-mail.').email('Email Inválido.'), 38 | password: yup 39 | .string() 40 | .required('Informe a senha.') 41 | .min(6, 'A senha deve ter pelo menos 6 digítos.') 42 | }) 43 | 44 | export function SignIn() { 45 | const [isLoading, setIsLoading] = useState(false) 46 | 47 | const { signIn } = useAuth() 48 | const navigation = useNavigation() 49 | const toast = useToast() 50 | 51 | const { 52 | control, 53 | handleSubmit, 54 | formState: { errors } 55 | } = useForm({ resolver: yupResolver(signInSchema) }) 56 | 57 | function handleNewAccount() { 58 | navigation.navigate('signUp') 59 | } 60 | 61 | async function handleSignIn({ email, password }: FormDataProps) { 62 | try { 63 | setIsLoading(true) 64 | await signIn(email, password) 65 | } catch (error) { 66 | const isAppError = error instanceof AppError 67 | 68 | setIsLoading(false) 69 | 70 | const title = isAppError 71 | ? error.message 72 | : 'Não foi possivel entrar. Tente novamente mais tarde.' 73 | 74 | toast.show({ title, placement: 'top', backgroundColor: 'red.500' }) 75 | } 76 | } 77 | 78 | return ( 79 | 83 | 89 | 90 | Pessoas Treinando 97 | 98 |
99 | 100 | 101 | 102 | Treine sua mente e o seu corpo 103 | 104 |
105 | 106 |
107 | 108 | Acesse sua conta 109 | 110 | 111 | ( 115 | 123 | )} 124 | /> 125 | 126 | ( 130 | 139 | )} 140 | /> 141 | 142 |
148 | 149 |
150 | 151 | Ainda não tem acesso? 152 | 153 | 154 |
160 |
161 |
162 |
163 | ) 164 | } 165 | -------------------------------------------------------------------------------- /mobile/src/screens/Exercise.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { TouchableOpacity } from 'react-native' 3 | import { useNavigation, useRoute } from '@react-navigation/native' 4 | import { 5 | Box, 6 | HStack, 7 | Heading, 8 | Icon, 9 | Image, 10 | Text, 11 | VStack, 12 | ScrollView, 13 | useToast 14 | } from 'native-base' 15 | import { Feather } from '@expo/vector-icons' 16 | 17 | import { api } from '@services/api' 18 | import { AppError } from '@utils/AppError' 19 | 20 | import { ExerciseDTO } from '@dtos/ExerciseDTO' 21 | 22 | import { AppNavigatorRoutesProps } from '@routes/app.routes' 23 | 24 | import BodySvg from '@assets/body.svg' 25 | import SeriesSvg from '@assets/series.svg' 26 | import RepetitionsSvg from '@assets/repetitions.svg' 27 | 28 | import { Button } from '@components/Button' 29 | import { Loading } from '@components/Loading' 30 | 31 | type RouteParamsProps = { 32 | exerciseId: string 33 | } 34 | 35 | export function Exercise() { 36 | const [sendingRegister, setSendingRegister] = useState(false) 37 | const [isLoading, setIsLoading] = useState(true) 38 | const [exercise, setExercise] = useState({} as ExerciseDTO) 39 | const navigation = useNavigation() 40 | 41 | const route = useRoute() 42 | const toast = useToast() 43 | 44 | const { exerciseId } = route.params as RouteParamsProps 45 | 46 | function handleGoBack() { 47 | navigation.goBack() 48 | } 49 | 50 | async function fetchExerciseDetails() { 51 | try { 52 | setIsLoading(true) 53 | 54 | const response = await api.get(`/exercises/${exerciseId}`) 55 | 56 | setExercise(response.data) 57 | } catch (error) { 58 | const isAppError = error instanceof AppError 59 | const title = isAppError 60 | ? error.message 61 | : 'Não foi possível carregar os detalhes do exercício.' 62 | 63 | toast.show({ 64 | title, 65 | placement: 'top', 66 | bgColor: 'red.500' 67 | }) 68 | } finally { 69 | setIsLoading(false) 70 | } 71 | } 72 | 73 | async function handleExerciseHistoryRegister() { 74 | try { 75 | setSendingRegister(true) 76 | 77 | await api.post('/history', { exercise_id: exerciseId }) 78 | 79 | toast.show({ 80 | title: 'Parabéns! Exercício registrado no seu histórico.', 81 | placement: 'top', 82 | bgColor: 'green.700' 83 | }) 84 | 85 | navigation.navigate('history') 86 | } catch (error) { 87 | const isAppError = error instanceof AppError 88 | const title = isAppError 89 | ? error.message 90 | : 'Não foi possível registrar o exercicio como realizado.' 91 | 92 | toast.show({ 93 | title, 94 | placement: 'top', 95 | bgColor: 'red.500' 96 | }) 97 | } finally { 98 | setSendingRegister(false) 99 | } 100 | } 101 | 102 | useEffect(() => { 103 | fetchExerciseDetails() 104 | }, [exerciseId]) 105 | 106 | return ( 107 | 108 | 109 | 110 | 111 | 112 | 113 | 119 | 125 | {exercise.name} 126 | 127 | 128 | 129 | 130 | 131 | {exercise.group} 132 | 133 | 134 | 135 | 136 | 137 | {isLoading ? ( 138 | 139 | ) : ( 140 | 141 | 142 | 143 | Nome do exercicio 155 | 156 | 157 | 158 | 164 | 165 | 166 | 167 | {exercise.series} Séries 168 | 169 | 170 | 171 | 172 | 173 | {exercise.repetitions} repetições 174 | 175 | 176 | 177 | 178 |