├── 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 |
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 |
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 |
147 |
148 |
149 |
150 |
151 | Ainda não tem acesso?
152 |
153 |
154 |
159 |
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 |
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 |
183 |
184 |
185 |
186 | )}
187 |
188 | )
189 | }
190 |
--------------------------------------------------------------------------------
/server/src/database/seeds/createExercises.js:
--------------------------------------------------------------------------------
1 | exports.seed = async function (knex) {
2 | await knex('exercises').del()
3 | await knex('exercises').insert([
4 | {
5 | name: 'Supino inclinado com barra',
6 | series: 4,
7 | repetitions: 12,
8 | group: 'peito',
9 | demo: 'supino_inclinado_com_barra.gif',
10 | thumb: 'supino_inclinado_com_barra.png',
11 | },
12 | {
13 | name: 'Crucifixo reto',
14 | series: 3,
15 | repetitions: 12,
16 | group: 'peito',
17 | demo: 'crucifixo_reto.gif',
18 | thumb: 'crucifixo_reto.png'
19 | },
20 | {
21 | name: 'Supino reto com barra',
22 | series: 3,
23 | repetitions: 12,
24 | group: 'peito',
25 | demo: 'supino_reto_com_barra.gif',
26 | thumb: 'supino_reto_com_barra.png'
27 | },
28 | {
29 | name: 'Francês deitado com halteres',
30 | series: 3,
31 | repetitions: 12,
32 | group: 'tríceps',
33 | demo: 'frances_deitado_com_halteres.gif',
34 | thumb: 'frances_deitado_com_halteres.png'
35 | },
36 | {
37 | name: 'Corda Cross',
38 | series: 4,
39 | repetitions: 12,
40 | group: 'tríceps',
41 | demo: 'corda_cross.gif',
42 | thumb: 'corda_cross.png'
43 | },
44 | {
45 | name: 'Barra Cross',
46 | series: 3,
47 | repetitions: 12,
48 | group: 'tríceps',
49 | demo: 'barra_cross.gif',
50 | thumb: 'barra_cross.png'
51 | },
52 | {
53 | name: 'Tríceps testa',
54 | series: 4,
55 | repetitions: 12,
56 | group: 'tríceps',
57 | demo: 'triceps_testa.gif',
58 | thumb: 'triceps_testa.png'
59 | },
60 | {
61 | name: 'Levantamento terra',
62 | series: 3,
63 | repetitions: 12,
64 | group: 'costas',
65 | demo: 'levantamento_terra.gif',
66 | thumb: 'levantamento_terra.png'
67 | },
68 | {
69 | name: 'Pulley frontal',
70 | series: 3,
71 | repetitions: 12,
72 | group: 'costas',
73 | demo: 'pulley_frontal.gif',
74 | thumb: 'pulley_frontal.png'
75 | },
76 | {
77 | name: 'Pulley atrás',
78 | series: 4,
79 | repetitions: 12,
80 | group: 'costas',
81 | demo: 'pulley_atras.gif',
82 | thumb: 'pulley_atras.png'
83 | },
84 | {
85 | name: 'Remada baixa',
86 | series: 4,
87 | repetitions: 12,
88 | group: 'costas',
89 | demo: 'remada_baixa.gif',
90 | thumb: 'remada_baixa.png'
91 | },
92 | {
93 | name: 'Serrote',
94 | series: 4,
95 | repetitions: 12,
96 | group: 'costas',
97 | demo: 'serrote.gif',
98 | thumb: 'serrote.png'
99 | },
100 | {
101 | name: 'Rosca alternada com banco inclinado',
102 | series: 4,
103 | repetitions: 12,
104 | group: 'bíceps',
105 | demo: 'rosca_alternada_com_banco_inclinado.gif',
106 | thumb: 'rosca_alternada_com_banco_inclinado.png'
107 | },
108 | {
109 | name: 'Rosca Scott barra w',
110 | series: 4,
111 | repetitions: 12,
112 | group: 'bíceps',
113 | demo: 'rosca_scott_barra_w.gif',
114 | thumb: 'rosca_scott_barra_w.png'
115 | },
116 | {
117 | name: 'Rosca direta barra reta',
118 | series: 3,
119 | repetitions: 12,
120 | group: 'bíceps',
121 | demo: 'rosca_direta_barra_reta.gif',
122 | thumb: 'rosca_direta_barra_reta.png'
123 | },
124 | {
125 | name: 'Martelo em pé',
126 | series: 3,
127 | repetitions: 12,
128 | group: 'bíceps',
129 | demo: 'martelo_em_pe.gif',
130 | thumb: 'martelo_em_pe.png'
131 | },
132 | {
133 | name: 'Rosca punho',
134 | series: 4,
135 | repetitions: 12,
136 | group: 'antebraço',
137 | demo: 'rosca_punho.gif',
138 | thumb: 'rosca_punho.png'
139 | },
140 | {
141 | name: 'Leg press 45 graus',
142 | series: 4,
143 | repetitions: 12,
144 | group: 'pernas',
145 | demo: 'leg_press_45_graus.gif',
146 | thumb: 'leg_press_45_graus.png'
147 | },
148 | {
149 | name: 'Extensor de pernas',
150 | series: 4,
151 | repetitions: 12,
152 | group: 'pernas',
153 | demo: 'extensor_de_pernas.gif',
154 | thumb: 'extensor_de_pernas.png'
155 | },
156 | {
157 | name: 'Abdutora',
158 | series: 4,
159 | repetitions: 12,
160 | group: 'pernas',
161 | demo: 'abdutora.gif',
162 | thumb: 'abdutora.png'
163 | },
164 | {
165 | name: 'Stiff',
166 | series: 4,
167 | repetitions: 12,
168 | group: 'pernas',
169 | demo: 'stiff.gif',
170 | thumb: 'stiff.png',
171 | },
172 | {
173 | name: 'Neck Press',
174 | series: 4,
175 | repetitions: 10,
176 | group: 'ombro',
177 | demo: 'neck-press.gif',
178 | thumb: 'neck-press.png'
179 | },
180 | {
181 | name: 'Desenvolvimento maquina',
182 | series: 3,
183 | repetitions: 10,
184 | group: 'ombro',
185 | demo: 'desenvolvimento_maquina.gif',
186 | thumb: 'desenvolvimento_maquina.png'
187 | },
188 | {
189 | name: 'Elevação lateral com halteres sentado',
190 | series: 4,
191 | repetitions: 10,
192 | group: 'ombro',
193 | demo: 'elevacao_lateral_com_halteres_sentado.gif',
194 | thumb: 'elevacao_lateral_com_halteres_sentado.png'
195 | },
196 | {
197 | name: 'Encolhimento com halteres',
198 | series: 4,
199 | repetitions: 10,
200 | group: 'trapézio',
201 | demo: 'encolhimento_com_halteres.gif',
202 | thumb: 'encolhimento_com_halteres.png'
203 | },
204 | {
205 | name: 'Encolhimento com barra',
206 | series: 4,
207 | repetitions: 10,
208 | group: 'trapézio',
209 | demo: 'encolhimento_com_barra.gif',
210 | thumb: 'encolhimento_com_barra.png'
211 | }
212 | ]);
213 | };
--------------------------------------------------------------------------------
/mobile/src/screens/SignUp.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 { useAuth } from '@hooks/useAuth'
9 |
10 | import { api } from '@services/api'
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 LogoSvg from '@assets/logo.svg'
24 | import BackgroundImg from '@assets/background.png'
25 |
26 | import { AppError } from '@utils/AppError'
27 |
28 | import { Input } from '@components/Input'
29 | import { Button } from '@components/Button'
30 |
31 | type FormDataProps = {
32 | name: string
33 | email: string
34 | password: string
35 | password_confirm: string
36 | }
37 |
38 | const signUpSchema = yup.object({
39 | name: yup.string().required('Informe o nome.'),
40 | email: yup.string().required('Informe o e-mail.').email('E-mail inválido.'),
41 | password: yup
42 | .string()
43 | .required('Informe a senha.')
44 | .min(6, 'A senha deve ter pelo menos 6 digítos.'),
45 | password_confirm: yup
46 | .string()
47 | .required('Confirme a senha.')
48 | .oneOf([yup.ref('password')], 'A confirmação da senha não confere.')
49 | })
50 |
51 | export function SignUp() {
52 | const [isLoading, setIsLoading] = useState(false)
53 |
54 | const toast = useToast()
55 | const { signIn } = useAuth()
56 |
57 | const {
58 | control,
59 | handleSubmit,
60 | formState: { errors }
61 | } = useForm({
62 | resolver: yupResolver(signUpSchema)
63 | })
64 |
65 | const navigation = useNavigation()
66 |
67 | function handleGoBack() {
68 | navigation.goBack()
69 | }
70 |
71 | async function handleSignUp({ name, email, password }: FormDataProps) {
72 | try {
73 | setIsLoading(true)
74 |
75 | await api.post('/users', { name, email, password })
76 | await signIn(email, password)
77 | } catch (error) {
78 | const isAppError = error instanceof AppError
79 |
80 | const title = isAppError
81 | ? error.message
82 | : 'Não foi possivel criar a conta. Tente novamente mais tarde.'
83 |
84 | toast.show({
85 | title,
86 | placement: 'top',
87 | bgColor: 'red.500'
88 | })
89 | } finally {
90 | setIsLoading(false)
91 | }
92 | }
93 |
94 | return (
95 |
99 |
105 |
106 |
113 |
114 |
115 |
116 |
117 |
118 | Treine sua mente e o seu corpo
119 |
120 |
121 |
122 |
123 |
124 | Crie sua conta
125 |
126 |
127 | (
131 |
137 | )}
138 | />
139 |
140 | (
144 |
152 | )}
153 | />
154 |
155 | (
159 |
166 | )}
167 | />
168 |
169 | (
173 |
182 | )}
183 | />
184 |
185 |
190 |
191 |
192 |
198 |
199 |
200 |
201 | )
202 | }
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
Ignite Gim
11 |
12 |
15 |
16 | Developed the Ignite Gym project in React Native, it is a fitness-oriented application that provides features for user registration, exercise tracking, and execution history.
17 |
18 | Below, I will describe each of the main screens of the application:
19 |
20 | - Login Screen:
21 |
22 | - On this screen, users can enter their login credentials, such as username and password, to access the application.
23 | - If they don't have an account yet, there is an option to register within the application.
24 |
25 | - Registration Screen:
26 |
27 | - In the registration screen, users can fill out a form with their information, such as name, email, and password.
28 | - After filling out the form, they can confirm the registration and create an account within the application.
29 |
30 | - Main Screen:
31 |
32 | - After successfully logging in, users will be directed to the main screen.
33 | - On this screen, they will find a list of available exercises to perform.
34 | - Each exercise can be selected to view the execution instructions, which may include a demonstrative video.
35 |
36 | - Workout History Screen:
37 |
38 | - In this screen, users can access the history of their previous workout sessions.
39 | - Users can revisit their previous sessions to track their progress over time.
40 |
41 | - Profile Screen:
42 |
43 | - The profile screen allows users to view and update their personal information, such as name, profile picture, and password change.
44 |
45 | These screens provide a comprehensive experience for users of the Ignite Gym application, allowing them to log in, register, perform exercises, track their progress, and manage their personal information.
46 |
47 |
48 |
49 | ## 🧭 Table of contents
50 |
51 | - [🧭 Table of contents](#-table-of-contents)
52 | - [🎥 Implementation Video](#-implementation-video)
53 | - [🎨 Layout](#-layout)
54 | - [👏 Learning and more Implementations](#-learning-and-more-implementations)
55 | - [💡 Technologies Used](#-technologies-used)
56 | - [Mobile](#mobile)
57 | - [📂 Folder Structure](#-folder-structure)
58 | - [🚀 Running the Project](#-running-the-project)
59 | - [Back-end](#back-end)
60 | - [Mobile](#mobile-1)
61 | - [🌎 License](#-license)
62 | - [✒ Author](#-author)
63 |
64 | ## 🎥 Implementation Video
65 |
66 | https://github.com/VagnerNerves/ignitegym-rn/assets/40831841/2ff96116-98e4-4641-9df6-7fa2c843e7b6
67 |
68 | ## 🎨 Layout
69 |
70 | Layout developed by: [Rodrigo Gonçalves](https://www.linkedin.com/in/rodrigo-goncalves-santana/) e [Millena Kupsinskü Martins](https://www.linkedin.com/in/millenakmartins/)
71 |
72 | []()
73 |
74 | ## 👏 Learning and more Implementations
75 |
76 | - Learned how to use NativeBase for building the interface.
77 | - Learned to use the Bottom Navigator.
78 | - Learned to fetch images from the photo gallery using Expo ImagePicker.
79 | - Learned to use React Hook Form for form control and validation using Yup.
80 | - Learned to create Contexts and hooks for data passing to other screens.
81 | - Learned about consuming APIs with Fetch API and Axios.
82 | - Learned about JWT Authentication and how to use it for data retrieval.
83 | - Learned to upload images to the database.
84 | - Learned about refresh tokens to automatically retrieve a new token when it expires.
85 |
86 | ## 💡 Technologies Used
87 |
88 | ### Mobile
89 |
90 | - [x] [React Native](https://reactnative.dev/)
91 | - [x] [Expo](https://docs.expo.dev/)
92 | - [x] [TypeScript](https://www.typescriptlang.org/)
93 | - [x] [NativeBase](https://nativebase.io/)
94 | - [x] [React Navigation - Native Stack and Bottom Tabs](https://reactnavigation.org/)
95 | - [x] [Axios](https://axios-http.com/ptbr/)
96 | - [x] [Expo ImagePicker](https://docs.expo.dev/versions/latest/sdk/imagepicker/)
97 | - [x] [Expo FileSystem](https://docs.expo.dev/versions/latest/sdk/filesystem/)
98 | - [x] [React Hook Form](https://react-hook-form.com/)
99 | - [x] [Yup](https://github.com/jquense/yup)
100 | - [x] [AsyncStorage](https://docs.expo.dev/versions/latest/sdk/async-storage/)
101 |
102 | ## 📂 Folder Structure
103 |
104 | ```plainText
105 | mobile
106 | .
107 | ├── assets # Images for expo
108 | ├── src # Source files
109 | │ ├── @types # Contains all global definitions of types and interfaces
110 | │ ├── assets # Contains Js bundles assets. e.g: icons, splash, images etc...
111 | │ ├── components # Contains all global react components
112 | │ ├── contexts # Application context
113 | │ ├── dtos # Models Data Base
114 | │ ├── hooks # Application hooks
115 | │ ├── routes # Contains application routes
116 | │ ├── screens # Contains application screens
117 | │ ├── services # Config service api
118 | │ ├── storage # Contains saving data in locations.
119 | │ ├── theme # Contains the theme of the application
120 | │ ├── utils # Class utils for system
121 | .
122 | .
123 | ├── App # Bundle entry
124 | .
125 | ```
126 |
127 | ## 🚀 Running the Project
128 |
129 | ### Back-end
130 |
131 | Clone the project
132 |
133 | ```bash
134 | git clone https://github.com/VagnerNerves/ignitegym-rn.git
135 | ```
136 |
137 | Enter the project directory
138 |
139 | ```bash
140 | cd ignitegym-rn\server
141 | ```
142 |
143 | Install with dependencies
144 |
145 | ```bash
146 | npm install
147 | ```
148 |
149 | Start the server
150 |
151 | ```bash
152 | npm run dev
153 | ```
154 |
155 | Access the README.md file in the server folder to see other commands.
156 |
157 |
182 |
183 | ### Mobile
184 |
185 | Clone the project
186 |
187 | ```bash
188 | git clone https://github.com/VagnerNerves/ignitegym-rn.git
189 | ```
190 |
191 | Enter the project directory
192 |
193 | ```bash
194 | cd ignitegym-rn\mobile
195 | ```
196 |
197 | Install with dependencies
198 |
199 | ```bash
200 | npm install
201 | ```
202 |
203 | Start the server
204 |
205 | ```bash
206 | npx run start
207 | ```
208 |
209 |
221 |
222 |
226 |
227 | ## 🌎 License
228 |
229 | This project is under the MIT license. See the [LICENSE](https://github.com/VagnerNerves/ignitegym-rn/blob/main/LICENSE) file for more details.
230 |
231 | ## ✒ Author
232 |
233 |
234 |
235 |
236 |
Vagner Nerves
237 |
238 |
239 | Made with love and hate 😅, get in touch!
240 |
241 |
242 |
243 |
244 |
245 | [](https://www.linkedin.com/in/vagnernervessantos/)
246 | [](mailto:vagnernervessantos@gmail.com)
247 | [](https://github.com/VagnerNerves)
248 |
249 |
250 |
--------------------------------------------------------------------------------
/README.pt-br.md:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
Ignite Gim
11 |
12 |
15 |
16 | O projeto Ignite Gim foi desenvolvido em React Native e é um aplicativo voltado para fitness que oferece recursos de registro de usuários, rastreamento de exercícios e histórico de execução.
17 |
18 | Abaixo, descreverei cada uma das principais telas do aplicativo:
19 |
20 | - Tela de Login:
21 |
22 | - Nesta tela, os usuários podem inserir suas credenciais de login, como nome de usuário e senha, para acessar o aplicativo.
23 | - Se eles ainda não possuem uma conta, há uma opção para se registrar dentro do próprio aplicativo.
24 |
25 | - Tela de Registro:
26 |
27 | - Na tela de registro, os usuários podem preencher um formulário com suas informações, como nome, e-mail e senha.
28 | - Após preencher o formulário, eles podem confirmar o registro e criar uma conta dentro do aplicativo.
29 |
30 | - Tela Principal:
31 |
32 | - Após fazer o login com sucesso, os usuários serão direcionados para a tela principal.
33 | - Nessa tela, eles encontrarão uma lista de exercícios disponíveis para realizar.
34 | - Cada exercício pode ser selecionado para visualizar as instruções de execução, que podem incluir um vídeo demonstrativo.
35 |
36 | - Tela de Histórico de Treino:
37 |
38 | - Nesta tela, os usuários podem acessar o histórico de suas sessões de treino anteriores.
39 | - Os usuários podem revisitar suas sessões anteriores para acompanhar seu progresso ao longo do tempo.
40 |
41 | - Tela de Perfil:
42 |
43 | - A tela de perfil permite que os usuários visualizem e atualizem suas informações pessoais, como nome, foto de perfil e alteração de senha.
44 |
45 | Essas telas proporcionam uma experiência abrangente para os usuários do aplicativo Ignite Gim, permitindo que eles façam login, se registrem, realizem exercícios, acompanhem seu progresso e gerenciem suas informações pessoais.
46 |
47 |
48 |
49 | ## 🧭 Índice
50 |
51 | - [🧭 Índice](#-índice)
52 | - [🎥 Vídeo de Implementação](#-vídeo-de-implementação)
53 | - [🎨 Layout](#-layout)
54 | - [👏 Aprendizado e Mais Implementações](#-aprendizado-e-mais-implementações)
55 | - [💡 Tecnologias Utilizadas](#-tecnologias-utilizadas)
56 | - [Mobile](#mobile)
57 | - [📂 Estrutura de Pastas](#-estrutura-de-pastas)
58 | - [🚀 Executando o Projeto](#-executando-o-projeto)
59 | - [Back-end](#back-end)
60 | - [Mobile](#mobile-1)
61 | - [🌎 Licença](#-licença)
62 | - [✒ Autor](#-autor)
63 |
64 | ## 🎥 Vídeo de Implementação
65 |
66 | https://github.com/VagnerNerves/ignitegym-rn/assets/40831841/2ff96116-98e4-4641-9df6-7fa2c843e7b6
67 |
68 | ## 🎨 Layout
69 |
70 | Layout desenvolvido por: [Rodrigo Gonçalves](https://www.linkedin.com/in/rodrigo-goncalves-santana/) e [Millena Kupsinskü Martins](https://www.linkedin.com/in/millenakmartins/)
71 |
72 | []()
73 |
74 | ## 👏 Aprendizado e Mais Implementações
75 |
76 | - Aprendi a usar o NativeBase para construir a interface.
77 | - Aprendi a usar o Bottom Navigator.
78 | - Aprendi a buscar imagens da galeria de fotos usando o Expo ImagePicker.
79 | - Aprendi a usar o React Hook Form para controle e validação de formulários usando o Yup.
80 | - Aprendi a criar Contextos e hooks para passar dados para outras telas.
81 | - Aprendi sobre o consumo de APIs com Fetch API e Axios.
82 | - Aprendi sobre Autenticação JWT e como usá-la para recuperar dados.
83 | - Aprendi a fazer upload de imagens para o banco de dados.
84 | - Aprendi sobre tokens de atualização para obter automaticamente um novo token quando ele expira.
85 |
86 | ## 💡 Tecnologias Utilizadas
87 |
88 | ### Mobile
89 |
90 | - [x] [React Native](https://reactnative.dev/)
91 | - [x] [Expo](https://docs.expo.dev/)
92 | - [x] [TypeScript](https://www.typescriptlang.org/)
93 | - [x] [NativeBase](https://nativebase.io/)
94 | - [x] [React Navigation - Native Stack and Bottom Tabs](https://reactnavigation.org/)
95 | - [x] [Axios](https://axios-http.com/ptbr/)
96 | - [x] [Expo ImagePicker](https://docs.expo.dev/versions/latest/sdk/imagepicker/)
97 | - [x] [Expo FileSystem](https://docs.expo.dev/versions/latest/sdk/filesystem/)
98 | - [x] [React Hook Form](https://react-hook-form.com/)
99 | - [x] [Yup](https://github.com/jquense/yup)
100 | - [x] [AsyncStorage](https://docs.expo.dev/versions/latest/sdk/async-storage/)
101 |
102 | ## 📂 Estrutura de Pastas
103 |
104 | ```plainText
105 | mobile
106 | .
107 | ├── assets # Imagens para o Expo
108 | ├── src # Arquivos de código-fonte
109 | │ ├── @types # Contém todas as definições globais de tipos e interfaces
110 | │ ├── assets # Contém os recursos de pacotes JS, como ícones, splash, imagens, etc.
111 | │ ├── components # Contém todos os componentes React globais
112 | │ ├── contexts # Contexto da aplicação
113 | │ ├── dtos # Modelos de banco de dados
114 | │ ├── hooks # Hooks da aplicação
115 | │ ├── routes # Contém as rotas da aplicação
116 | │ ├── screens # Contém as telas da aplicação
117 | │ ├── services # Configurações do serviço de API
118 | │ ├── storage # Contém a persistência de dados em locais específicos
119 | │ ├── theme # Contém o tema da aplicação
120 | │ ├── utils # Classes utilitárias para o sistema
121 | .
122 | .
123 | ├── App # Ponto de entrada do pacote
124 | .
125 | ```
126 |
127 | ## 🚀 Executando o Projeto
128 |
129 | ### Back-end
130 |
131 | Clone o projeto
132 |
133 | ```bash
134 | git clone https://github.com/VagnerNerves/ignitegym-rn.git
135 | ```
136 |
137 | Acesse o diretório do projeto
138 |
139 | ```bash
140 | cd ignitegym-rn\server
141 | ```
142 |
143 | Instale as dependências
144 |
145 | ```bash
146 | npm install
147 | ```
148 |
149 | Inicie o servidor
150 |
151 | ```bash
152 | npm run dev
153 | ```
154 |
155 | Acesse o arquivo README.md na pasta do servidor para ver outros comandos.
156 |
157 |
182 |
183 | ### Mobile
184 |
185 | Clone o projeto
186 |
187 | ```bash
188 | git clone https://github.com/VagnerNerves/ignitegym-rn.git
189 | ```
190 |
191 | Acesse o diretório do projeto
192 |
193 | ```bash
194 | cd ignitegym-rn\mobile
195 | ```
196 |
197 | Instale as dependências
198 |
199 | ```bash
200 | npm install
201 | ```
202 |
203 | Inicie o servidor
204 |
205 | ```bash
206 | npx run start
207 | ```
208 |
209 |
221 |
222 |
226 |
227 | ## 🌎 Licença
228 |
229 | Este projeto está sob a licença MIT. Veja o arquivo [LICENSE](https://github.com/VagnerNerves/ignitegym-rn/blob/main/LICENSE) para mais detalhes.
230 |
231 | ## ✒ Autor
232 |
233 |
234 |
235 |
236 |
Vagner Nerves
237 |
238 |
239 | Feito com amor e ódio 😅, entre em contato!
240 |
241 |
242 |
243 |
244 |
245 | [](https://www.linkedin.com/in/vagnernervessantos/)
246 | [](mailto:vagnernervessantos@gmail.com)
247 | [](https://github.com/VagnerNerves)
248 |
249 |
250 |
--------------------------------------------------------------------------------
/server/insomnia.json:
--------------------------------------------------------------------------------
1 | {"_type":"export","__export_format":4,"__export_date":"2022-08-24T00:27:09.945Z","__export_source":"insomnia.desktop.app:v2022.5.0","resources":[{"_id":"req_a3d2ba4340e24c9abde875e0bf0a29af","parentId":"fld_0315834da4c74bb88989d166a8543e1b","modified":1661261262650,"created":1661198920486,"url":"{{ _.BASE_URL }}/files/346dab6b457abadbbb2a-49030804.jpg","name":"Show","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"type":"bearer","token":"{{ _.AUTH_TOKEN }}"},"metaSortKey":-1661256495499.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_0315834da4c74bb88989d166a8543e1b","parentId":"wrk_778e7b61ae4d4a049fc5baa0b2d05756","modified":1661261320086,"created":1661258473500,"name":"Image","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1661258473500,"_type":"request_group"},{"_id":"wrk_778e7b61ae4d4a049fc5baa0b2d05756","parentId":null,"modified":1661195093886,"created":1661195093886,"name":"Rocket.Gym","description":"","scope":"collection","_type":"workspace"},{"_id":"req_b5b19c3d23e44fa1b9a668803a62477e","parentId":"fld_6c98f22849e743728d7a7a1543080ec1","modified":1661295001356,"created":1661254518612,"url":"{{ _.BASE_URL }}/{{ _.RESOURCE }}","name":"Index By User","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"type":"bearer","token":"{{ _.AUTH_TOKEN }}"},"metaSortKey":-1661254518612,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_6c98f22849e743728d7a7a1543080ec1","parentId":"wrk_778e7b61ae4d4a049fc5baa0b2d05756","modified":1661261123384,"created":1661254517499,"name":"History","description":"","environment":{"RESOURCE":"history"},"environmentPropertyOrder":{"&":["RESOURCE"]},"metaSortKey":-1661254517499,"_type":"request_group"},{"_id":"req_9dfdc4942051485d997aad167cd468d4","parentId":"fld_6c98f22849e743728d7a7a1543080ec1","modified":1661294938040,"created":1661255702195,"url":"{{ _.BASE_URL }}/{{ _.RESOURCE }}","name":"Create","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"exercise_id\": 2\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_e6572b047c2e4c89b3c044d8315ef547"}],"authentication":{"type":"bearer","token":"{{ _.AUTH_TOKEN }}"},"metaSortKey":-1661254221615,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_23de25dbe468437b95374dd753192960","parentId":"fld_e94f81acb4ec49ada6445978a5d2319f","modified":1661261056505,"created":1661253924618,"url":"{{ _.BASE_URL }}/{{ _.RESOURCE }}","name":"Index","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1661253924618,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_e94f81acb4ec49ada6445978a5d2319f","parentId":"wrk_778e7b61ae4d4a049fc5baa0b2d05756","modified":1661261052020,"created":1661253923543,"name":"Group","description":"","environment":{"RESOURCE":"groups"},"environmentPropertyOrder":{"&":["RESOURCE"]},"metaSortKey":-1661253923543,"_type":"request_group"},{"_id":"req_1aab82fcdef6474bae07ac90a951bf12","parentId":"fld_2f35276e2ef84a1b9befa1f855673683","modified":1661260496403,"created":1661253455308,"url":"{{ _.BASE_URL }}/{{ _.RESOURCE }}/bygroup/bíceps","name":"Index By Group","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1661253455308,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_2f35276e2ef84a1b9befa1f855673683","parentId":"wrk_778e7b61ae4d4a049fc5baa0b2d05756","modified":1661260491089,"created":1661253454245,"name":"Exercise","description":"","environment":{"RESOURCE":"exercises"},"environmentPropertyOrder":{"&":["RESOURCE"]},"metaSortKey":-1661253454245,"_type":"request_group"},{"_id":"req_5d88a9acb2724017a453295db0784c1d","parentId":"fld_2f35276e2ef84a1b9befa1f855673683","modified":1661260500241,"created":1661254368363,"url":"{{ _.BASE_URL }}/{{ _.RESOURCE }}/1","name":"Show","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1661224498435,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_cac712764ccc49b8ac20a358a6604150","parentId":"fld_f12023b30db943cf90b354f7c8ae5b12","modified":1661294856577,"created":1661195541562,"url":"{{ _.BASE_URL }}/{{ _.RESOURCE }}","name":"Create","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Rodrigo\",\n\t\"email\": \"rodrigo@email.com\",\n\t\"password\": \"123\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_578d0226e65444e3b7dc1c6bbcb028a5"}],"authentication":{},"metaSortKey":-1661195541562,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_f12023b30db943cf90b354f7c8ae5b12","parentId":"wrk_778e7b61ae4d4a049fc5baa0b2d05756","modified":1661258442664,"created":1661195539794,"name":"User","description":"","environment":{"RESOURCE":"users"},"environmentPropertyOrder":{"&":["RESOURCE"]},"metaSortKey":-1661195539794,"_type":"request_group"},{"_id":"req_63a8c41efd494aa3812953d6085ad781","parentId":"fld_f12023b30db943cf90b354f7c8ae5b12","modified":1661294857369,"created":1661196252588,"url":"{{ _.BASE_URL }}/{{ _.RESOURCE }}","name":"Update","description":"","method":"PUT","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Rodrigo Gonçalves\",\n\t\"password\": \"1234\",\n\t\"old_password\": \"123\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_578d0226e65444e3b7dc1c6bbcb028a5"}],"authentication":{"type":"bearer","token":"{{ _.AUTH_TOKEN }}"},"metaSortKey":-1661195349514,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_8a06a056f473430e8ad07394323f73fc","parentId":"fld_f12023b30db943cf90b354f7c8ae5b12","modified":1661294860323,"created":1661198716636,"url":"{{ _.BASE_URL }}/{{ _.RESOURCE }}/avatar","name":"Avatar Update","description":"","method":"PATCH","body":{"mimeType":"multipart/form-data","params":[{"name":"avatar","value":"","id":"pair_a86afda6fabc4efb95e255a61917f314","type":"file","fileName":"C:\\Users\\rodrigo.gsantana1\\Downloads\\49030804.jpg"}]},"parameters":[],"headers":[{"name":"Content-Type","value":"multipart/form-data","id":"pair_578d0226e65444e3b7dc1c6bbcb028a5"}],"authentication":{"type":"bearer","token":"{{ _.AUTH_TOKEN }}"},"metaSortKey":-1661195253490,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_0630eb49889145e19258699e982ae3ea","parentId":"fld_2fe464311a7247ad8c192fee9b810275","modified":1661294985616,"created":1661195157466,"url":"{{ _.BASE_URL }}/{{ _.RESOURCE }}","name":"Create","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"email\": \"rodrigo@email.com\",\n\t\"password\": \"123\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_50d8d1e0a45f44648cbcb612ed899cbc"}],"authentication":{},"metaSortKey":-1661195157466,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_2fe464311a7247ad8c192fee9b810275","parentId":"wrk_778e7b61ae4d4a049fc5baa0b2d05756","modified":1661275375722,"created":1661195155735,"name":"Session","description":"","environment":{"RESOURCE":"sessions"},"environmentPropertyOrder":{"&":["RESOURCE"]},"metaSortKey":-1661195155736,"_type":"request_group"},{"_id":"req_6c89b36b605041fe80762d6c4dc29bee","parentId":"fld_2fe464311a7247ad8c192fee9b810275","modified":1661294999306,"created":1661274026470,"url":"{{ _.BASE_URL }}/{{ _.RESOURCE }}/refresh-token","name":"Refresh Token","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"token\": \"{% response 'body', 'req_0630eb49889145e19258699e982ae3ea', 'b64::JC5yZWZyZXNoX3Rva2Vu::46b', 'never', 60 %}\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_77c44643859f408490a7aafacbfd706e"}],"authentication":{},"metaSortKey":-1661195157416,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"env_333766c9d26198b561c37471af31fd5b163fa768","parentId":"wrk_778e7b61ae4d4a049fc5baa0b2d05756","modified":1661195105001,"created":1661195093891,"name":"Base Environment","data":{},"dataPropertyOrder":{},"color":null,"isPrivate":false,"metaSortKey":1661195093891,"_type":"environment"},{"_id":"jar_333766c9d26198b561c37471af31fd5b163fa768","parentId":"wrk_778e7b61ae4d4a049fc5baa0b2d05756","modified":1661195093892,"created":1661195093892,"name":"Default Jar","cookies":[],"_type":"cookie_jar"},{"_id":"spc_0f88b40036aa4f729ed66af428f7dbd0","parentId":"wrk_778e7b61ae4d4a049fc5baa0b2d05756","modified":1661195093887,"created":1661195093887,"fileName":"Rocket.Gym","contents":"","contentType":"yaml","_type":"api_spec"},{"_id":"env_9a82d1795bd1430b98226497e42f058b","parentId":"env_333766c9d26198b561c37471af31fd5b163fa768","modified":1661294980270,"created":1661195106683,"name":"development","data":{"BASE_URL":"localhost:3333","AUTH_TOKEN":"{% response 'body', 'req_6c89b36b605041fe80762d6c4dc29bee', 'b64::JA==::46b', 'never', 60 %}"},"dataPropertyOrder":{"&":["BASE_URL","AUTH_TOKEN"]},"color":"#1eff00","isPrivate":false,"metaSortKey":1661195106683,"_type":"environment"}]}
--------------------------------------------------------------------------------
/mobile/src/screens/Profile.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { TouchableOpacity, Platform } from 'react-native'
3 | import { useForm, Controller } from 'react-hook-form'
4 | import { yupResolver } from '@hookform/resolvers/yup'
5 | import * as yup from 'yup'
6 |
7 | import { useAuth } from '@hooks/useAuth'
8 |
9 | import defaultUserPhotoImg from '@assets/userPhotoDefault.png'
10 |
11 | import {
12 | Center,
13 | ScrollView,
14 | VStack,
15 | Skeleton,
16 | Text,
17 | Heading,
18 | useToast,
19 | KeyboardAvoidingView
20 | } from 'native-base'
21 |
22 | import { api } from '@services/api'
23 | import { AppError } from '@utils/AppError'
24 |
25 | import * as ImagePicker from 'expo-image-picker'
26 | import * as FileSystem from 'expo-file-system'
27 |
28 | import { ScreenHeader } from '@components/ScreenHeader'
29 | import { UserPhoto } from '@components/UserPhoto'
30 | import { Input } from '@components/Input'
31 | import { Button } from '@components/Button'
32 |
33 | const PHOTO_SIZE = 33
34 |
35 | type FileSystemProps = FileSystem.FileInfo & {
36 | size: number
37 | }
38 |
39 | type FormDataProps = {
40 | name: string
41 | email: string
42 | old_password: string
43 | password: string
44 | confirm_password: string
45 | }
46 |
47 | const profileSchema = yup.object({
48 | name: yup.string().required('Informe o name.'),
49 | email: yup.string(),
50 | old_password: yup
51 | .string()
52 | .min(6, 'A senha antiga deve ter pelo menos 6 digítos.')
53 | .nullable()
54 | .transform(value => (!!value ? value : null)),
55 | password: yup
56 | .string()
57 | .min(6, 'A senha nova deve ter pelo menos 6 digítos.')
58 | .nullable()
59 | .transform(value => (!!value ? value : null)),
60 | confirm_password: yup
61 | .string()
62 | .nullable()
63 | .transform(value => (!!value ? value : null))
64 | .oneOf([yup.ref('password')], 'A confirmação da senha não confere.')
65 | .when('password', {
66 | is: (Field: any) => Field && Field !== null,
67 | then: schema =>
68 | schema
69 | .nullable()
70 | .required('Informe a confirmação da senha.')
71 | .transform(value => (!!value ? value : null))
72 | })
73 | })
74 |
75 | export function Profile() {
76 | const [updating, setUpdating] = useState(false)
77 | const [photoIsLoading, setPhotoIsLoading] = useState(false)
78 |
79 | const toast = useToast()
80 | const { user, updateUserProfile } = useAuth()
81 |
82 | const {
83 | control,
84 | handleSubmit,
85 | formState: { errors },
86 | setValue
87 | } = useForm({
88 | defaultValues: {
89 | name: user.name,
90 | email: user.email
91 | },
92 | resolver: yupResolver(profileSchema)
93 | })
94 |
95 | async function handleUserPhotoSelect() {
96 | setPhotoIsLoading(true)
97 |
98 | try {
99 | const photoSelected = await ImagePicker.launchImageLibraryAsync({
100 | mediaTypes: ImagePicker.MediaTypeOptions.Images,
101 | quality: 1,
102 | aspect: [4, 4],
103 | allowsEditing: true
104 | })
105 |
106 | if (photoSelected.canceled) {
107 | return
108 | }
109 |
110 | if (photoSelected.assets[0].uri) {
111 | const photoInfo = (await FileSystem.getInfoAsync(
112 | photoSelected.assets[0].uri
113 | )) as FileSystemProps
114 |
115 | if (photoInfo.size && photoInfo.size / 1024 / 1024 > 5) {
116 | return toast.show({
117 | title: 'Essa imagem é muito grande. Escolha uma de até 5MB.',
118 | placement: 'top',
119 | bgColor: 'red.500'
120 | })
121 | }
122 |
123 | const fileExtension = photoSelected.assets[0].uri.split('.').pop()
124 |
125 | const photFile = {
126 | name: `${user.name}.${fileExtension}`.toLowerCase(),
127 | uri: photoSelected.assets[0].uri,
128 | type: `${photoSelected.assets[0].type}/${fileExtension}`
129 | } as any
130 |
131 | const userPhotUploadForm = new FormData()
132 | userPhotUploadForm.append('avatar', photFile)
133 |
134 | const avatarUpdatedResponse = await api.patch(
135 | '/users/avatar',
136 | userPhotUploadForm,
137 | {
138 | headers: { 'Content-type': 'multipart/form-data' }
139 | }
140 | )
141 |
142 | toast.show({
143 | title: 'Foto Atualizada.',
144 | placement: 'top',
145 | bgColor: 'green.500'
146 | })
147 |
148 | const userUpdated = user
149 | userUpdated.avatar = avatarUpdatedResponse.data.avatar
150 | updateUserProfile(userUpdated)
151 | }
152 | } catch (error) {
153 | console.log(error)
154 | } finally {
155 | setPhotoIsLoading(false)
156 | }
157 | }
158 |
159 | async function handleProfileUpdate(data: FormDataProps) {
160 | try {
161 | setUpdating(true)
162 |
163 | const userUpdated = user
164 | userUpdated.name = data.name
165 |
166 | await api.put('/users', data)
167 |
168 | await updateUserProfile(userUpdated)
169 |
170 | setValue('password', '')
171 | setValue('old_password', '')
172 | setValue('confirm_password', '')
173 |
174 | toast.show({
175 | title: 'Perfil atualizado com sucesso!',
176 | placement: 'top',
177 | bgColor: 'green.500'
178 | })
179 | } catch (error) {
180 | const isAppError = error instanceof AppError
181 | const title = isAppError
182 | ? error.message
183 | : 'Não foi possível atualizar os dados. Tente novamente mais tarde.'
184 |
185 | toast.show({
186 | title,
187 | placement: 'top',
188 | bgColor: 'red.500'
189 | })
190 | } finally {
191 | setUpdating(false)
192 | }
193 | }
194 |
195 | return (
196 |
197 |
198 |
199 |
203 |
208 |
209 | {photoIsLoading ? (
210 |
217 | ) : (
218 |
227 | )}
228 |
229 |
230 |
237 | Alterar foto
238 |
239 |
240 |
241 | (
245 |
252 | )}
253 | />
254 |
255 | (
259 |
268 | )}
269 | />
270 |
271 |
279 | Alterar senha
280 |
281 |
282 | (
286 |
294 | )}
295 | />
296 |
297 | (
301 |
309 | )}
310 | />
311 | (
315 |
325 | )}
326 | />
327 |
328 |
334 |
335 |
336 |
337 |
338 | )
339 | }
340 |
--------------------------------------------------------------------------------
/mobile/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/server/src/docs/swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.0",
3 | "info": {
4 | "title": "Ignite Gym API",
5 | "description": "API developed by Rodrigo Gonçalves to be used in Ignite training in the mobile backend integration.",
6 | "version": "1.0.0",
7 | "contact": {
8 | "email": "rodrigo.rgtic@gmail.com"
9 | }
10 | },
11 | "paths": {
12 | "/users": {
13 | "post": {
14 | "tags": [
15 | "User"
16 | ],
17 | "summary": "Create",
18 | "description": "Create a new user",
19 | "requestBody": {
20 | "content": {
21 | "application/json": {
22 | "schema": {
23 | "type": "object",
24 | "properties": {
25 | "name": {
26 | "type": "string"
27 | },
28 | "email": {
29 | "type": "string"
30 | },
31 | "password": {
32 | "type": "string"
33 | }
34 | },
35 | "example": {
36 | "name": "Rodrigo",
37 | "email": "rodrigo@email.com",
38 | "password": "123"
39 | }
40 | }
41 | }
42 | }
43 | },
44 | "responses": {
45 | "201": {
46 | "description": "Created"
47 | },
48 | "400": {
49 | "description": "Bad Request"
50 | }
51 | }
52 | },
53 | "put": {
54 | "tags": [
55 | "User"
56 | ],
57 | "summary": "Update",
58 | "description": "Update user profile",
59 | "requestBody": {
60 | "content": {
61 | "application/json": {
62 | "schema": {
63 | "type": "object",
64 | "properties": {
65 | "name": {
66 | "type": "string",
67 | "required": false
68 | },
69 | "password": {
70 | "type": "string",
71 | "required": false
72 | },
73 | "old_password": {
74 | "type": "string",
75 | "required": false
76 | }
77 | },
78 | "example": {
79 | "name": "Rodrigo Gonçalves",
80 | "password": "1234",
81 | "old_password": "123"
82 | }
83 | }
84 | }
85 | }
86 | },
87 | "responses": {
88 | "200": {
89 | "description": "Updated"
90 | },
91 | "400": {
92 | "description": "Bad Request"
93 | },
94 | "404": {
95 | "description": "User not found"
96 | }
97 | }
98 | }
99 | },
100 | "/users/avatar": {
101 | "patch": {
102 | "tags": [
103 | "User"
104 | ],
105 | "summary": "Upload",
106 | "description": "Update user profile picture",
107 | "requestBody": {
108 | "content": {
109 | "multipart/form-data": {
110 | "schema": {
111 | "type": "object",
112 | "properties": {
113 | "avatar": {
114 | "type": "string",
115 | "format": "base64"
116 | }
117 | },
118 | "example": {
119 | "avatar": "rodrigo.png"
120 | }
121 | },
122 | "encoding": {
123 | "avatar": {
124 | "contentType": "image/png, image/jpeg"
125 | }
126 | }
127 | }
128 | }
129 | },
130 | "responses": {
131 | "200": {
132 | "description": "Updated"
133 | },
134 | "400": {
135 | "description": "Bad Request"
136 | },
137 | "401": {
138 | "description": "Not authorized"
139 | }
140 | }
141 | }
142 | },
143 | "/sessions": {
144 | "post": {
145 | "tags": [
146 | "User"
147 | ],
148 | "summary": "Sign In",
149 | "description": "User authentication",
150 | "requestBody": {
151 | "content": {
152 | "application/json": {
153 | "schema": {
154 | "type": "object",
155 | "properties": {
156 | "email": {
157 | "type": "string"
158 | },
159 | "password": {
160 | "type": "string"
161 | }
162 | },
163 | "example": {
164 | "email": "rodrigo@email.com",
165 | "password": "123"
166 | }
167 | }
168 | }
169 | }
170 | },
171 | "responses": {
172 | "201": {
173 | "description": "Authenticated",
174 | "content": {
175 | "application/json": {
176 | "schema": {
177 | "type": "object",
178 | "example": {
179 | "id": 1,
180 | "name": "Rodrigo Gonçalves",
181 | "email": "rodrigo@email.com",
182 | "avatar": "346dab6b457abadbbb2a-49030804.jpg",
183 | "created_at": "2022-08-22 19:59:46",
184 | "updated_at": "2022-08-22T20:07:45.340Z"
185 | }
186 | }
187 | }
188 | }
189 | },
190 | "400": {
191 | "description": "Bad Request"
192 | },
193 | "401": {
194 | "description": "Not authorized/Invalid email or password"
195 | }
196 | }
197 | }
198 | },
199 | "/sessions/refresh-token": {
200 | "post": {
201 | "tags": [
202 | "User"
203 | ],
204 | "summary": "Refresh Token",
205 | "description": "Auth Refresh Token",
206 | "requestBody": {
207 | "content": {
208 | "application/json": {
209 | "schema": {
210 | "type": "object",
211 | "properties": {
212 | "token": {
213 | "type": "string"
214 | }
215 | },
216 | "example": {
217 | "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InJvZHJpZ29AZW1haWwuY29tIiwiaWF0IjoxNjYxMjc1NDAxLCJleHAiOjE2NjM4Njc0MDEsInN1YiI6IjEifQ.yQqqvmuZrF9ZM0LThzIu8dlwQtmuHdG0C_nwziXWyMo"
218 | }
219 | }
220 | }
221 | }
222 | },
223 | "responses": {
224 | "201": {
225 | "description": "Created",
226 | "content": {
227 | "application/json": {
228 | "schema": {
229 | "type": "string",
230 | "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InJvZHJpZ29AZW1haWwuY29tIiwiaWF0IjoxNjYxMjc1NDE5LCJleHAiOjE2NjM4Njc0MTksInN1YiI6IjEifQ.kQoOrRyGvSkLcFS49ItDcLUEB7pEhbwyPRoEA5sR4ao"
231 | }
232 | }
233 | }
234 | },
235 | "400": {
236 | "description": "Bad Request"
237 | },
238 | "404": {
239 | "description": "Refresh token not found"
240 | }
241 | }
242 | }
243 | },
244 | "/exercises": {
245 | "get": {
246 | "tags": [
247 | "Exercise"
248 | ],
249 | "summary": "Index",
250 | "description": "List all exercises",
251 | "requestBody": {
252 | "content": {
253 | "application/json": {
254 | "schema": {
255 | "type": "object",
256 | "example": [
257 | {
258 | "id": 16,
259 | "name": "Martelo em pé",
260 | "series": 3,
261 | "repetitions": "10 a 12",
262 | "group": "bíceps",
263 | "created_at": "2022-08-23 11:12:32",
264 | "updated_at": "2022-08-23 11:12:32"
265 | },
266 | {
267 | "id": 13,
268 | "name": "Rosca alternada com banco inclinado",
269 | "series": 4,
270 | "repetitions": "10 a 12",
271 | "group": "bíceps",
272 | "created_at": "2022-08-23 11:12:32",
273 | "updated_at": "2022-08-23 11:12:32"
274 | },
275 | {
276 | "id": 15,
277 | "name": "Rosca direta barra reta",
278 | "series": 3,
279 | "repetitions": "10 a 12",
280 | "group": "bíceps",
281 | "created_at": "2022-08-23 11:12:32",
282 | "updated_at": "2022-08-23 11:12:32"
283 | },
284 | {
285 | "id": 14,
286 | "name": "Rosca scott barra w",
287 | "series": 4,
288 | "repetitions": "10 a 12",
289 | "group": "bíceps",
290 | "created_at": "2022-08-23 11:12:32",
291 | "updated_at": "2022-08-23 11:12:32"
292 | }
293 | ]
294 | }
295 | }
296 | }
297 | },
298 | "responses": {
299 | "200": {
300 | "description": "Success"
301 | },
302 | "400": {
303 | "description": "Bad Request"
304 | }
305 | }
306 | }
307 | },
308 | "/exercises/{id}": {
309 | "get": {
310 | "tags": [
311 | "Exercise"
312 | ],
313 | "summary": "Index",
314 | "description": "List all exercises",
315 | "parameters": [
316 | {
317 | "in": "path",
318 | "name": "id",
319 | "required": true
320 | }
321 | ],
322 | "requestBody": {
323 | "content": {
324 | "application/json": {
325 | "schema": {
326 | "type": "object",
327 | "example": {
328 | "id": 1,
329 | "name": "Supino inclinado com barra",
330 | "series": 4,
331 | "repetitions": "10 a 12",
332 | "group": "peito",
333 | "created_at": "2022-08-23 11:12:32",
334 | "updated_at": "2022-08-23 11:12:32"
335 | }
336 | }
337 | }
338 | }
339 | },
340 | "responses": {
341 | "200": {
342 | "description": "Success"
343 | },
344 | "400": {
345 | "description": "Bad Request"
346 | }
347 | }
348 | }
349 | },
350 | "/groups": {
351 | "get": {
352 | "tags": [
353 | "Group"
354 | ],
355 | "summary": "Index",
356 | "description": "List all groups",
357 | "requestBody": {
358 | "content": {
359 | "application/json": {
360 | "schema": {
361 | "type": "object",
362 | "example": [
363 | "Trapézio",
364 | "antebraço",
365 | "bíceps",
366 | "costas",
367 | "ombro",
368 | "peito",
369 | "pernas",
370 | "tríceps"
371 | ]
372 | }
373 | }
374 | }
375 | },
376 | "responses": {
377 | "200": {
378 | "description": "Success"
379 | },
380 | "400": {
381 | "description": "Bad Request"
382 | }
383 | }
384 | }
385 | },
386 | "/history": {
387 | "get": {
388 | "tags": [
389 | "history"
390 | ],
391 | "summary": "Index",
392 | "description": "List history by user",
393 | "requestBody": {
394 | "content": {
395 | "application/json": {
396 | "schema": {
397 | "type": "object",
398 | "example": [
399 | {
400 | "id": 1,
401 | "user_id": 1,
402 | "exercise_id": 1,
403 | "name": "Supino inclinado com barra",
404 | "group": "peito",
405 | "created_at": "2022-08-23 11:55:29"
406 | },
407 | {
408 | "id": 2,
409 | "user_id": 1,
410 | "exercise_id": 2,
411 | "name": "Supino inclinado com barra",
412 | "group": "peito",
413 | "created_at": "2022-08-23 12:16:01"
414 | }
415 | ]
416 | }
417 | }
418 | }
419 | },
420 | "responses": {
421 | "200": {
422 | "description": "Success"
423 | },
424 | "400": {
425 | "description": "Bad Request"
426 | }
427 | }
428 | },
429 | "post": {
430 | "tags": [
431 | "history"
432 | ],
433 | "summary": "Index",
434 | "description": "Create user exercise history ",
435 | "requestBody": {
436 | "content": {
437 | "application/json": {
438 | "schema": {
439 | "type": "object",
440 | "properties": {
441 | "exercise_id": {
442 | "type": "number"
443 | }
444 | },
445 | "example": [
446 | {
447 | "id": 1,
448 | "user_id": 1,
449 | "exercise_id": 1,
450 | "name": "Supino inclinado com barra",
451 | "group": "peito",
452 | "created_at": "2022-08-23 11:55:29"
453 | },
454 | {
455 | "id": 2,
456 | "user_id": 1,
457 | "exercise_id": 2,
458 | "name": "Supino inclinado com barra",
459 | "group": "peito",
460 | "created_at": "2022-08-23 12:16:01"
461 | }
462 | ]
463 | }
464 | }
465 | }
466 | },
467 | "responses": {
468 | "201": {
469 | "description": "Created"
470 | },
471 | "400": {
472 | "description": "Bad Request"
473 | }
474 | }
475 | }
476 | },
477 | "/files/${filename.png}": {
478 | "get": {
479 | "tags": [
480 | "Image"
481 | ],
482 | "summary": "Show",
483 | "description": "Show image file",
484 | "parameters": [
485 | {
486 | "in": "path",
487 | "name": "filename",
488 | "required": true
489 | }
490 | ],
491 | "responses": {
492 | "200": {
493 | "description": "Success"
494 | },
495 | "400": {
496 | "description": "Bad Request"
497 | }
498 | }
499 | }
500 | }
501 | }
502 | }
--------------------------------------------------------------------------------