├── .gitignore ├── README.md ├── arquitetura.png ├── server ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── knexfile.ts ├── package-lock.json ├── package.json ├── rotas.txt ├── src │ ├── controllers │ │ ├── ClassesController.ts │ │ └── ConnectionsController.ts │ ├── database │ │ ├── connection.ts │ │ ├── database.sqlite │ │ └── migrations │ │ │ ├── 00_create_users.ts │ │ │ ├── 01_create_classes.ts │ │ │ ├── 02_create_class_schedule.ts │ │ │ └── 03_create_connections.ts │ ├── routes.ts │ ├── server.ts │ └── utils │ │ └── convertHourToMinutes.ts ├── tsconfig.json └── yarn.lock └── web ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── cypress.json ├── cypress ├── component │ └── PageHeader.spec.js ├── fixtures │ └── example.json ├── integration │ ├── api │ │ └── connections.spec.js │ └── ui │ │ ├── landing.spec.js │ │ └── teacher-list.spec.js ├── plugins │ └── index.js ├── support │ ├── commands.js │ └── index.js └── videos │ ├── PageHeader.spec.js.mp4 │ ├── api │ └── connections.spec.js.mp4 │ └── ui │ ├── landing.spec.js.mp4 │ └── teacher-list.spec.js.mp4 ├── package-lock.json ├── package.json ├── public └── index.html ├── src ├── App.tsx ├── assets │ ├── images │ │ ├── icons │ │ │ ├── back.svg │ │ │ ├── give-classes.svg │ │ │ ├── purple-heart.svg │ │ │ ├── rocket.svg │ │ │ ├── smile.svg │ │ │ ├── study.svg │ │ │ ├── success-check-icon.svg │ │ │ ├── warning.svg │ │ │ └── whatsapp.svg │ │ ├── landing.svg │ │ ├── logo.svg │ │ └── success-background.svg │ └── styles │ │ └── global.css ├── components │ ├── Input │ │ ├── index.tsx │ │ └── styles.css │ ├── PageHeader │ │ ├── index.tsx │ │ └── styles.css │ ├── Select │ │ ├── index.tsx │ │ └── styles.css │ ├── TeacherItem │ │ ├── index.tsx │ │ └── styles.css │ └── Textarea │ │ ├── index.tsx │ │ └── styles.css ├── index.tsx ├── pages │ ├── Landing │ │ ├── index.tsx │ │ └── styles.css │ ├── TeacherForm │ │ ├── index.tsx │ │ └── styles.css │ └── TeacherList │ │ ├── index.tsx │ │ └── styles.css ├── react-app-env.d.ts ├── routes.tsx └── services │ └── api.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # misc 2 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Cypress MultiLevel Testing

2 | 3 |

O que é Cypress?

4 |

Cypress é uma ferramenta para testes de aplicações em múltiplos níveis. Veja isso na prática

5 | 6 |

Acompanhe o Agilizei ⚡️

7 | 8 | # Aplicação 9 | 10 | Neste projeto, estamos utilizando a aplicação *Proffy* desenvolvida da NLW #02 da RocketSeat. 11 | 12 | # Arquitetura 13 | 14 |

15 | 16 | # Níveis de testes 17 | 18 | ## Componente 19 | Podemos executar testes de componentes utilizando o Cypress. Neste projeto, usamos: 20 | 21 | - cypress-react-unit-test 22 | 23 | Exemplo: 24 | ```js 25 | it('deve ser renderizado com sucesso', () => { 26 | mount( 27 | 28 | 32 | 33 | ) 34 | 35 | cy.get('strong').should('have.text', 'TDC2020'); 36 | ``` 37 | 38 | ## API 39 | Podemos executar testes de APIs utilizando o Cypress. Neste projeto, usamos: 40 | 41 | - cy-api (apenas para melhorar a visibilidade do que está sendo enviado/recebido) 42 | 43 | Exemplo: 44 | ```js 45 | it('GET deve retornar status 200', () => { 46 | 47 | cy.api({ 48 | method: 'GET', 49 | url: `${Cypress.env('API_URL')}/connections` 50 | }).then((connectionsResponse) => { 51 | expect(connectionsResponse.status).to.eq(200) 52 | }) 53 | }); 54 | ``` 55 | 56 | 57 | ## UI 58 | Podemos executar testes de UI utilizando o Cypress. 59 | 60 | Exemplo: 61 | ```js 62 | it('Acessar a página inicial com conexões realizadas', () => { 63 | cy.visit('/'); 64 | 65 | cy.get('span.total-connections').should('contain.text', '5'); 66 | }); 67 | ``` 68 | 69 | -------------------------------------------------------------------------------- /arquitetura.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlucax/cypress-multilevel-testing/c3dc56f280b186d2513ec5c747e56c7eb34705b6/arquitetura.png -------------------------------------------------------------------------------- /server/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = false 8 | insert_final_newline = true 9 | spaces_around_operators = true 10 | end_of_line = lf 11 | max_line_length = off 12 | 13 | [*.py] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [*.md] 18 | indent_size = false 19 | trim_trailing_whitespace = false 20 | 21 | [*.java] 22 | indent_style = tab -------------------------------------------------------------------------------- /server/.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/dist 3 | **/package.json 4 | **/yarn.lock 5 | **/package-lock.json 6 | **/.eslintrc.js 7 | **/tsconfig.json 8 | **/knexfile.ts 9 | -------------------------------------------------------------------------------- /server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2020": true, 4 | "node": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": 11, 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint"], 13 | "rules": { 14 | "no-var": "error", 15 | "linebreak-style": ["error", "unix"], 16 | "eqeqeq": "error", 17 | "comma-dangle": ["error", "always-multiline"], 18 | "import/no-mutable-exports": 0, 19 | "no-labels": 0, 20 | "no-restricted-syntax": 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /server/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "printWidth": 100, 4 | "singleQuote": true, 5 | "jsxSingleQuote": false, 6 | "trailingComma": "all", 7 | "tabWidth": 2, 8 | "useTabs": true, 9 | "quoteProps": "as-needed", 10 | "bracketSpacing": true, 11 | "jsxBracketSameLine": false, 12 | "endOfLine": "lf", 13 | "arrowParens": "avoid" 14 | } 15 | -------------------------------------------------------------------------------- /server/knexfile.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | module.exports = { 4 | client: 'sqlite3', 5 | connection: { 6 | filename: path.resolve(__dirname, 'src', 'database', 'database.sqlite'), 7 | }, 8 | migrations: { 9 | directory: path.resolve(__dirname, 'src', 'database', 'migrations'), 10 | }, 11 | useNullAsDefault: true, 12 | }; 13 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "@typescript-eslint/eslint-plugin": "^3.8.0", 8 | "@typescript-eslint/parser": "^3.8.0", 9 | "eslint": "^7.6.0", 10 | "eslint-config-prettier": "^6.11.0", 11 | "eslint-plugin-prettier": "^3.1.4", 12 | "prettier": "^2.0.5", 13 | "ts-node-dev": "^1.0.0-pre.56", 14 | "typescript": "^3.9.7" 15 | }, 16 | "scripts": { 17 | "start": "tsnd --transpile-only --ignore-watch node_modules --respawn src/server.ts", 18 | "knex:migrate": "knex --knexfile knexfile.ts migrate:latest", 19 | "knex:rollback": "knex --knexfile knexfile.ts migrate:rollback", 20 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx --quiet", 21 | "lint-fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix" 22 | }, 23 | "dependencies": { 24 | "@types/cors": "^2.8.7", 25 | "@types/express": "^4.17.7", 26 | "cors": "^2.8.5", 27 | "express": "^4.17.1", 28 | "knex": "^0.21.2", 29 | "sqlite3": "^5.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/rotas.txt: -------------------------------------------------------------------------------- 1 | - rota para listar o total de conexoes realizadas 2 | - rota para criar uma aula 3 | - listar aulas 4 | - filtrar pro materia, dia da semana, horario 5 | -------------------------------------------------------------------------------- /server/src/controllers/ClassesController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import db from '../database/connection'; 3 | import convertHourToMinutes from '../utils/convertHourToMinutes'; 4 | 5 | interface ScheduleItem { 6 | week_day: number; 7 | from: string; 8 | to: string; 9 | } 10 | 11 | export default class ClassesController { 12 | async create(request: Request, response: Response): Promise { 13 | const { name, avatar, whatsapp, bio, subject, cost, schedule } = request.body; 14 | 15 | const trx = await db.transaction(); 16 | 17 | try { 18 | const insertedUsersIds = await trx('users').insert({ 19 | name, 20 | avatar, 21 | whatsapp, 22 | bio, 23 | }); 24 | 25 | const user_id = insertedUsersIds[0]; 26 | 27 | const insertedClassesIds = await trx('classes').insert({ 28 | subject, 29 | cost, 30 | user_id, 31 | }); 32 | 33 | const class_id = insertedClassesIds[0]; 34 | 35 | const classSchedule = schedule.map((scheduleItem: ScheduleItem) => { 36 | return { 37 | class_id, 38 | week_day: scheduleItem.week_day, 39 | from: convertHourToMinutes(scheduleItem.from), 40 | to: convertHourToMinutes(scheduleItem.to), 41 | }; 42 | }); 43 | 44 | await trx('class_schedule').insert(classSchedule); 45 | 46 | await trx.commit(); 47 | 48 | return response.status(201).send(); 49 | } catch (error) { 50 | await trx.rollback(); 51 | return response.status(400).json({ 52 | error: 'Unexpected error while creating new class', 53 | }); 54 | } 55 | } 56 | 57 | async index(request: Request, response: Response): Promise { 58 | const filters = request.query; 59 | 60 | const subject = filters.subject as string; 61 | const week_day = filters.week_day as string; 62 | const time = filters.time as string; 63 | 64 | if (!filters.week_day || !filters.subject || !filters.time) { 65 | return response.status(400).json({ 66 | error: 'Missing filters to search classes.', 67 | }); 68 | } 69 | 70 | const timeInMinutes = convertHourToMinutes(time); 71 | const classes = await db('classes') 72 | .whereExists(function () { 73 | this.select('class_schedule.*') 74 | .from('class_schedule') 75 | .whereRaw('`class_schedule`.`class_id` = `classes`.`id`') 76 | .whereRaw('`class_schedule`.`week_day` = ??', [Number(week_day)]) 77 | .whereRaw('`class_schedule`.`from` <= ??', [timeInMinutes]) 78 | .whereRaw('`class_schedule`.`to` > ??', [timeInMinutes]); 79 | }) 80 | .where('classes.subject', '=', subject) 81 | .join('users', 'classes.user_id', '=', 'users.id') 82 | .select(['classes.*', 'users.*']); 83 | 84 | return response.send(classes); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /server/src/controllers/ConnectionsController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import db from '../database/connection'; 3 | 4 | export default class ConnectionsController { 5 | async index(request: Request, response: Response): Promise { 6 | const totalConnections = await db('connections').count('* as total'); 7 | 8 | const { total } = totalConnections[0]; 9 | 10 | return response.json({ total }); 11 | } 12 | 13 | async create(request: Request, response: Response): Promise { 14 | const { user_id } = request.body; 15 | 16 | await db('connections').insert({ 17 | user_id, 18 | }); 19 | 20 | return response.status(201).send(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/src/database/connection.ts: -------------------------------------------------------------------------------- 1 | import knex from 'knex'; 2 | import path from 'path'; 3 | 4 | const db = knex({ 5 | client: 'sqlite3', 6 | connection: { 7 | filename: path.resolve(__dirname, 'database.sqlite'), 8 | }, 9 | useNullAsDefault: true, 10 | }); 11 | 12 | export default db; 13 | -------------------------------------------------------------------------------- /server/src/database/database.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlucax/cypress-multilevel-testing/c3dc56f280b186d2513ec5c747e56c7eb34705b6/server/src/database/database.sqlite -------------------------------------------------------------------------------- /server/src/database/migrations/00_create_users.ts: -------------------------------------------------------------------------------- 1 | import Knex from "knex"; 2 | 3 | export async function up(knex: Knex): Promise { 4 | return knex.schema.createTable('users', table => { 5 | table.increments('id').primary(); 6 | table.string('name').notNullable(); 7 | table.string('avatar').notNullable(); 8 | table.string('whatsapp').notNullable(); 9 | table.string('bio').notNullable(); 10 | }); 11 | } 12 | 13 | export async function down(knex: Knex): Promise { 14 | return knex.schema.dropTable('users'); 15 | } 16 | -------------------------------------------------------------------------------- /server/src/database/migrations/01_create_classes.ts: -------------------------------------------------------------------------------- 1 | import Knex from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | return knex.schema.createTable('classes', table => { 5 | table.increments('id').primary(); 6 | table.string('subject').notNullable(); 7 | table.decimal('cost').notNullable(); 8 | 9 | table 10 | .integer('user_id') 11 | .notNullable() 12 | .references('id') 13 | .inTable('users') 14 | .onUpdate('CASCADE') 15 | .onDelete('CASCADE'); 16 | }); 17 | } 18 | 19 | export async function down(knex: Knex): Promise { 20 | return knex.schema.dropTable('classes'); 21 | } 22 | -------------------------------------------------------------------------------- /server/src/database/migrations/02_create_class_schedule.ts: -------------------------------------------------------------------------------- 1 | import Knex from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | return knex.schema.createTable('class_schedule', table => { 5 | table.increments('id').primary(); 6 | 7 | table.integer('week_day').notNullable(); 8 | table.integer('from').notNullable(); 9 | table.integer('to').notNullable(); 10 | 11 | table 12 | .integer('class_id') 13 | .notNullable() 14 | .references('id') 15 | .inTable('classes') 16 | .onUpdate('CASCADE') 17 | .onDelete('CASCADE'); 18 | }); 19 | } 20 | 21 | export async function down(knex: Knex): Promise { 22 | return knex.schema.dropTable('class_schedule'); 23 | } 24 | -------------------------------------------------------------------------------- /server/src/database/migrations/03_create_connections.ts: -------------------------------------------------------------------------------- 1 | import Knex from 'knex'; 2 | 3 | export async function up(knex: Knex): Promise { 4 | return knex.schema.createTable('connections', table => { 5 | table.increments('id').primary(); 6 | 7 | table 8 | .integer('user_id') 9 | .notNullable() 10 | .references('id') 11 | .inTable('users') 12 | .onUpdate('CASCADE') 13 | .onDelete('CASCADE'); 14 | 15 | table.timestamp('created_at').defaultTo(knex.raw('CURRENT_TIMESTAMP')).notNullable(); 16 | }); 17 | } 18 | 19 | export async function down(knex: Knex): Promise { 20 | return knex.schema.dropTable('connections'); 21 | } 22 | -------------------------------------------------------------------------------- /server/src/routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import ClassesController from './controllers/ClassesController'; 3 | import ConnectionsController from './controllers/ConnectionsController'; 4 | 5 | const routes = express.Router(); 6 | const classesController = new ClassesController(); 7 | const connectionsController = new ConnectionsController(); 8 | 9 | routes.post('/classes', classesController.create); 10 | routes.get('/classes', classesController.index); 11 | 12 | routes.post('/connections', connectionsController.create); 13 | routes.get('/connections', connectionsController.index); 14 | 15 | export default routes; 16 | -------------------------------------------------------------------------------- /server/src/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import cors from 'cors'; 3 | 4 | import routes from './routes'; 5 | 6 | const app = express(); 7 | 8 | app.use(cors()); 9 | app.use(express.json()); 10 | 11 | app.use(routes); 12 | 13 | app.listen(3333); 14 | -------------------------------------------------------------------------------- /server/src/utils/convertHourToMinutes.ts: -------------------------------------------------------------------------------- 1 | export default function convertHourToMinutes(time: string): number { 2 | const [hour, minutes] = time.split(':').map(Number); 3 | const timeInMinutes = (hour * 60) + minutes; 4 | 5 | return timeInMinutes; 6 | } 7 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /web/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = false 8 | insert_final_newline = true 9 | spaces_around_operators = true 10 | end_of_line = lf 11 | max_line_length = off 12 | 13 | [*.py] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [*.md] 18 | indent_size = false 19 | trim_trailing_whitespace = false 20 | 21 | [*.java] 22 | indent_style = tab -------------------------------------------------------------------------------- /web/.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/dist 3 | **/package.json 4 | **/yarn.lock 5 | **/package-lock.json 6 | **/.eslintrc.js 7 | **/tsconfig.json -------------------------------------------------------------------------------- /web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "settings": { 7 | "react": { 8 | "version": "detect" 9 | } 10 | }, 11 | "extends": [ 12 | "airbnb-typescript", 13 | "plugin:@typescript-eslint/recommended", 14 | "plugin:eslint-comments/recommended", 15 | "plugin:promise/recommended", 16 | "prettier", 17 | "prettier/@typescript-eslint" 18 | ], 19 | "globals": { 20 | "Atomics": "readonly", 21 | "SharedArrayBuffer": "readonly" 22 | }, 23 | "parser": "@typescript-eslint/parser", 24 | "parserOptions": { 25 | "project": ["./tsconfig.json"], 26 | "ecmaFeatures": { 27 | "jsx": true 28 | }, 29 | "ecmaVersion": 2018, 30 | "sourceType": "module" 31 | }, 32 | "plugins": ["react", "@typescript-eslint", "svelte3"], 33 | "rules": { 34 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }], 35 | "no-var": "error", 36 | "linebreak-style": ["error", "unix"], 37 | "eqeqeq": "error", 38 | "comma-dangle": ["error", "always-multiline"], 39 | "import/no-mutable-exports": 0, 40 | "no-labels": 0, 41 | "no-restricted-syntax": 0, 42 | "react/require-default-props": [2, { "forbidDefaultForRequired": false, "ignoreFunctionalComponents": true }], 43 | "jsx-a11y/label-has-associated-control": [0], 44 | "react/jsx-props-no-spreading": "off", 45 | "promise/always-return": "off", 46 | "promise/catch-or-return": "off", 47 | "react/jsx-no-target-blank": "off", 48 | "react/no-array-index-key": "off", 49 | "no-alert": "off" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | .nyc_output 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | .idea 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "printWidth": 140, 4 | "singleQuote": true, 5 | "jsxSingleQuote": false, 6 | "trailingComma": "es5", 7 | "tabWidth": 2, 8 | "useTabs": true, 9 | "quoteProps": "as-needed", 10 | "bracketSpacing": true, 11 | "jsxBracketSameLine": false, 12 | "endOfLine": "lf", 13 | "arrowParens": "avoid" 14 | } 15 | -------------------------------------------------------------------------------- /web/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000", 3 | "experimentalComponentTesting": true, 4 | "componentFolder": "cypress/component", 5 | "env": { 6 | "API_URL": "http://localhost:3333", 7 | "coverage": true 8 | }, 9 | "projectId": "3tt4fk" 10 | } 11 | -------------------------------------------------------------------------------- /web/cypress/component/PageHeader.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import React from 'react'; 4 | 5 | import PageHeader from '../../src/components/PageHeader'; 6 | import { mount } from 'cypress-react-unit-test'; 7 | 8 | import { BrowserRouter as Router } from 'react-router-dom' 9 | 10 | describe('PageHeader component', () => { 11 | const baseUrl = '/__root/src/assets/styles/global.css' 12 | const indexUrl = '/__root/src/components/PageHeader/styles.css' 13 | 14 | it('deve ser renderizado com sucesso', () => { 15 | mount( 16 | 17 | 21 | 22 | , 23 | { 24 | stylesheets: [baseUrl, indexUrl] 25 | } 26 | ) 27 | 28 | cy.get('strong').should('have.text', 'QArentena 44'); 29 | 30 | cy.get('.header-content').as('header'); 31 | cy.get('@header').find('strong').as('title'); 32 | cy.get('@header').children('p').as('description'); 33 | 34 | cy.get('@title').should('have.text', 'QArentena 44'); 35 | cy.get('@description').should('have.text', 'Trocando uma ideia sobre Cypress de ponta a ponta'); 36 | }); 37 | }); 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | // react components 51 | // componente -> menor unidade de uma interface 52 | // peça de um quebra cabeça 53 | // lego, com algumas peças mais versáteis -> propriedades do componente -------------------------------------------------------------------------------- /web/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /web/cypress/integration/api/connections.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | describe('Connections endpoint', () => { 5 | it('GET deve retornar status 200', () => { 6 | 7 | cy.api({ 8 | method: 'GET', 9 | url: `${Cypress.env('API_URL')}/connections` 10 | }).then((connectionsResponse) => { 11 | expect(connectionsResponse.status).to.eq(200) 12 | }) 13 | }); 14 | }); -------------------------------------------------------------------------------- /web/cypress/integration/ui/landing.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Landing Page', () => { 4 | it('Acessar a página inicial com conexões realizadas', () => { 5 | cy.visit('/'); 6 | 7 | cy.get('span.total-connections').should('contain.text', '5'); 8 | }); 9 | 10 | it('Acessar a página inicial sem conexões', () => { 11 | const total4Example = 999; 12 | 13 | // mocks 14 | cy.server(); 15 | cy.route({ 16 | method: 'GET', 17 | url: `**/connections`, 18 | response: { total: total4Example }, 19 | status: 200 20 | }).as('GETConnections'); 21 | 22 | cy.visit('/'); 23 | 24 | cy.get('span.total-connections').should('contain.text', `${total4Example}`); 25 | }); 26 | }); -------------------------------------------------------------------------------- /web/cypress/integration/ui/teacher-list.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('TeacherList', () => { 4 | it('Acessar a listagem de professores disponíveis ', () => { 5 | cy.visit('http://localhost:3000/study'); 6 | }); 7 | }); -------------------------------------------------------------------------------- /web/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = (on, config) => { 19 | require('cypress-react-unit-test/plugins/react-scripts')(on, config) 20 | 21 | return config 22 | } 23 | -------------------------------------------------------------------------------- /web/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /web/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | require('cypress-react-unit-test/support') 19 | import '@bahmutov/cy-api/support' 20 | // Alternatively you can use CommonJS syntax: 21 | // require('./commands') 22 | -------------------------------------------------------------------------------- /web/cypress/videos/PageHeader.spec.js.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlucax/cypress-multilevel-testing/c3dc56f280b186d2513ec5c747e56c7eb34705b6/web/cypress/videos/PageHeader.spec.js.mp4 -------------------------------------------------------------------------------- /web/cypress/videos/api/connections.spec.js.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlucax/cypress-multilevel-testing/c3dc56f280b186d2513ec5c747e56c7eb34705b6/web/cypress/videos/api/connections.spec.js.mp4 -------------------------------------------------------------------------------- /web/cypress/videos/ui/landing.spec.js.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlucax/cypress-multilevel-testing/c3dc56f280b186d2513ec5c747e56c7eb34705b6/web/cypress/videos/ui/landing.spec.js.mp4 -------------------------------------------------------------------------------- /web/cypress/videos/ui/teacher-list.spec.js.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlucax/cypress-multilevel-testing/c3dc56f280b186d2513ec5c747e56c7eb34705b6/web/cypress/videos/ui/teacher-list.spec.js.mp4 -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "@types/jest": "^24.0.0", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^16.9.0", 12 | "@types/react-dom": "^16.9.0", 13 | "@types/react-router-dom": "^5.1.5", 14 | "axios": "^0.19.2", 15 | "react": "^16.13.1", 16 | "react-dom": "^16.13.1", 17 | "react-router-dom": "^5.2.0", 18 | "react-scripts": "3.4.1", 19 | "typescript": "~3.7.2" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "start:inst": "react-scripts -r @cypress/instrument-cra start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject", 27 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx --quiet", 28 | "lint-fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", 29 | "coverage:report": "nyc report --reporter=html", 30 | "test:component": "cypress run --spec 'cypress/component/**'", 31 | "test:api": "cypress run --spec 'cypress/integration/api/**'", 32 | "test:ui": "cypress run --spec 'cypress/integration/ui/**'", 33 | "test:dashboard": "cypress run --record --key 03cead8b-829b-438a-b867-069c34a3ee39" 34 | }, 35 | "eslintConfig": { 36 | "extends": "react-app" 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | }, 50 | "devDependencies": { 51 | "@bahmutov/cy-api": "^1.4.2", 52 | "@cypress/instrument-cra": "^1.3.1", 53 | "@typescript-eslint/eslint-plugin": "^3.8.0", 54 | "@typescript-eslint/parser": "^3.8.0", 55 | "cypress": "^4.10.0", 56 | "cypress-react-unit-test": "^4.11.2", 57 | "eslint": "6.8.0", 58 | "eslint-config-airbnb-typescript": "^9.0.0", 59 | "eslint-config-prettier": "^6.11.0", 60 | "eslint-plugin-eslint-comments": "^3.2.0", 61 | "eslint-plugin-import": "^2.22.0", 62 | "eslint-plugin-prettier": "^3.1.4", 63 | "eslint-plugin-promise": "^4.2.1", 64 | "eslint-plugin-react": "^7.20.5", 65 | "eslint-plugin-react-hooks": "^4.0.8", 66 | "eslint-plugin-svelte3": "^2.7.3", 67 | "nyc": "^15.1.0", 68 | "prettier": "^2.0.5" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | Proffy 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | 3 | import Routes from './routes'; 4 | 5 | import './assets/styles/global.css'; 6 | 7 | function App(): ReactElement { 8 | return ; 9 | } 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /web/src/assets/images/icons/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/src/assets/images/icons/give-classes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /web/src/assets/images/icons/purple-heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/src/assets/images/icons/rocket.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /web/src/assets/images/icons/smile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /web/src/assets/images/icons/study.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /web/src/assets/images/icons/success-check-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /web/src/assets/images/icons/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /web/src/assets/images/icons/whatsapp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /web/src/assets/images/landing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | -------------------------------------------------------------------------------- /web/src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /web/src/assets/images/success-background.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /web/src/assets/styles/global.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-background: #f0f0f7; 3 | --color-primary-lighter: #9871f5; 4 | --color-primary-light: #916bea; 5 | --color-primary: #8257e5; 6 | --color-primary-dark: #774dd6; 7 | --color-primary-darker: #6842c2; 8 | --color-secundary: #04d361; 9 | --color-secundary-dark: #04bf58; 10 | --color-title-in-primary: #ffffff; 11 | --color-text-in-primary: #d4c2ff; 12 | --color-text-title: #32264d; 13 | --color-text-complement: #9c98a6; 14 | --color-text-base: #6a6180; 15 | --color-line-in-white: #e6e6f0; 16 | --color-input-background: #f8f8fc; 17 | --color-button-text: #ffffff; 18 | --color-box-base: #ffffff; 19 | --color-box-footer: #fafafc; 20 | 21 | font-size: 60%; 22 | } 23 | 24 | * { 25 | margin: 0; 26 | padding: 0; 27 | box-sizing: border-box; 28 | } 29 | 30 | hmtl, 31 | body, 32 | #root { 33 | height: 100vh; 34 | } 35 | 36 | body { 37 | background: var(--color-background); 38 | } 39 | 40 | #root { 41 | display: flex; 42 | align-items: center; 43 | justify-content: center; 44 | } 45 | 46 | body, 47 | input, 48 | button, 49 | textarea { 50 | font: 500 1.6rem Poppins; 51 | color: var(--color-text-base); 52 | } 53 | 54 | .container { 55 | width: 90vw; 56 | max-width: 700px; 57 | } 58 | 59 | @media (min-width: 700px) { 60 | :root { 61 | font-size: 62.5%; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /web/src/components/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { InputHTMLAttributes } from 'react'; 2 | 3 | import './styles.css'; 4 | 5 | interface InputProps extends InputHTMLAttributes { 6 | label: string; 7 | name: string; 8 | } 9 | 10 | const Input:React.FC = ({ label, name, ...rest }: InputProps) => { 11 | return ( 12 |
13 | 14 | 15 |
16 | ); 17 | } 18 | 19 | export default Input; 20 | -------------------------------------------------------------------------------- /web/src/components/Input/styles.css: -------------------------------------------------------------------------------- 1 | .input-block { 2 | position: relative; 3 | } 4 | 5 | .input-block + .input-block { 6 | margin-top: 1.4rem; 7 | } 8 | 9 | .input-block label { 10 | font-size: 1.4rem; 11 | } 12 | 13 | .input-block input { 14 | width: 100%; 15 | height: 5.6rem; 16 | margin-top: 0.8rem; 17 | border-radius: 0.8rem; 18 | background: var(--color-input-background); 19 | border: 1px solid var(--color-line-in-white); 20 | outline: 0; 21 | padding: 0 1.6rem; 22 | font: 1.6rem Archivo; 23 | } 24 | 25 | .input-block:focus-within::after { 26 | width: calc(100% - 3.2rem); 27 | height: 2px; 28 | content: ''; 29 | background: var(--color-primary-light); 30 | position: absolute; 31 | left: 1.6rem; 32 | right: 1.6rem; 33 | bottom: 0; 34 | } 35 | -------------------------------------------------------------------------------- /web/src/components/PageHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import logoImg from '../../assets/images/logo.svg'; 5 | import backIcon from '../../assets/images/icons/back.svg'; 6 | 7 | import './styles.css'; 8 | 9 | interface PageHeaderProps { 10 | title: string; 11 | description?: string; 12 | children?: ReactNode; 13 | } 14 | 15 | const PageHeader: React.FC = ({ title, description, children }: PageHeaderProps) => { 16 | return ( 17 |
18 |
19 | 20 | Voltar 21 | 22 | Proffy 23 |
24 | 25 |
26 | {title} 27 | {description &&

{description}

} 28 | 29 | {children} 30 |
31 |
32 | ); 33 | }; 34 | 35 | export default PageHeader; 36 | -------------------------------------------------------------------------------- /web/src/components/PageHeader/styles.css: -------------------------------------------------------------------------------- 1 | .page-header { 2 | display: flex; 3 | flex-direction: column; 4 | background-color: var(--color-primary); 5 | } 6 | 7 | .page-header .top-bar-container { 8 | width: 90%; 9 | margin: 0 auto; 10 | display: flex; 11 | justify-content: space-between; 12 | align-items: center; 13 | color: var(--color-text-in-primary); 14 | padding: 1.6rem 0; 15 | } 16 | 17 | .page-header .top-bar-container a { 18 | height: 3.2rem; 19 | transition: opacity 0.2s; 20 | } 21 | 22 | .page-header .top-bar-container a:hover { 23 | opacity: 0.6; 24 | } 25 | 26 | .page-header .top-bar-container > img { 27 | height: 1.6rem; 28 | } 29 | 30 | .page-header .header-content { 31 | width: 90%; 32 | margin: 0 auto; 33 | position: relative; 34 | margin: 3.2rem auto; 35 | } 36 | 37 | .page-header .header-content strong { 38 | font: 700 3.6rem Archivo; 39 | line-height: 4.2rem; 40 | color: var(--color-title-in-primary); 41 | } 42 | 43 | .page-header .header-content p { 44 | max-width: 30rem; 45 | font-size: 1.6rem; 46 | line-height: 2.6rem; 47 | color: var(--color-text-in-primary); 48 | margin-top: 2.4rem; 49 | } 50 | 51 | @media (min-width: 700px) { 52 | .page-header { 53 | height: 340px; 54 | } 55 | 56 | .page-header .top-bar-container { 57 | max-width: 1100px; 58 | } 59 | 60 | .page-header .header-content { 61 | flex: 1; 62 | max-width: 740px; 63 | margin: 0 auto; 64 | padding-bottom: 48px; 65 | display: flex; 66 | flex-direction: column; 67 | justify-content: center; 68 | align-items: flex-start; 69 | } 70 | 71 | .page-header .header-content strong { 72 | max-width: 350px; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /web/src/components/Select/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { SelectHTMLAttributes } from 'react'; 2 | 3 | import './styles.css'; 4 | 5 | interface SelectProps extends SelectHTMLAttributes { 6 | label: string; 7 | name: string; 8 | options: Array<{ 9 | value: string; 10 | label: string; 11 | }>; 12 | } 13 | 14 | const Select:React.FC = ({ label, name, options, ...rest }: SelectProps) => { 15 | return ( 16 |
17 | 18 | 24 |
25 | ); 26 | } 27 | 28 | export default Select; 29 | -------------------------------------------------------------------------------- /web/src/components/Select/styles.css: -------------------------------------------------------------------------------- 1 | .select-block { 2 | position: relative; 3 | } 4 | 5 | .select-block + .select-block { 6 | margin-top: 1.4rem; 7 | } 8 | 9 | .select-block label { 10 | font-size: 1.4rem; 11 | } 12 | 13 | .select-block select { 14 | width: 100%; 15 | height: 5.6rem; 16 | margin-top: 0.8rem; 17 | border-radius: 0.8rem; 18 | background: var(--color-input-background); 19 | border: 1px solid var(--color-line-in-white); 20 | outline: 0; 21 | padding: 0 1.6rem; 22 | font: 1.6rem Archivo; 23 | } 24 | 25 | .select-block:focus-within::after { 26 | width: calc(100% - 3.2rem); 27 | height: 2px; 28 | content: ''; 29 | background: var(--color-primary-light); 30 | position: absolute; 31 | left: 1.6rem; 32 | right: 1.6rem; 33 | bottom: 0; 34 | } 35 | -------------------------------------------------------------------------------- /web/src/components/TeacherItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './styles.css'; 4 | 5 | import whastappIcon from '../../assets/images/icons/whatsapp.svg'; 6 | import api from '../../services/api'; 7 | 8 | export interface Teacher { 9 | avatar: string; 10 | bio: string; 11 | cost: number; 12 | id: number; 13 | name: string; 14 | subject: string; 15 | whatsapp: number; 16 | } 17 | 18 | interface TeacherItemProps { 19 | teacher: Teacher; 20 | } 21 | 22 | const TeacherItem: React.FC = ({ teacher }: TeacherItemProps) => { 23 | function createNewConnection() { 24 | api.post('connections', { 25 | user_id: teacher.id, 26 | }); 27 | } 28 | 29 | return ( 30 |
31 |
32 | {teacher.name} 33 |
34 | {teacher.name} 35 | {teacher.subject} 36 |
37 |
38 |

39 | {teacher.bio} 40 |

41 | 59 |
60 | ); 61 | } 62 | 63 | export default TeacherItem; 64 | -------------------------------------------------------------------------------- /web/src/components/TeacherItem/styles.css: -------------------------------------------------------------------------------- 1 | .teacher-item { 2 | background: var(--color-box-base); 3 | border: 1px solid var(--color-line-in-white); 4 | border-radius: 0.8rem; 5 | margin-top: 2.4rem; 6 | overflow: hidden; 7 | } 8 | 9 | .teacher-item header { 10 | padding: 3.2rem 2rem; 11 | display: flex; 12 | align-items: center; 13 | } 14 | 15 | .teacher-item header img { 16 | width: 8rem; 17 | height: 8rem; 18 | border-radius: 50%; 19 | } 20 | 21 | .teacher-item header div { 22 | margin-left: 2.4rem; 23 | } 24 | 25 | .teacher-item header div strong { 26 | font: 700 2.4rem Archivo; 27 | display: block; 28 | color: var(--color-text-title); 29 | } 30 | 31 | .teacher-item header div span { 32 | font: 1.6rem; 33 | display: block; 34 | margin-top: 0.4rem; 35 | } 36 | 37 | .teacher-item > p { 38 | padding: 0 2rem; 39 | font-size: 1.6rem; 40 | line-height: 2.8rem; 41 | } 42 | 43 | .teacher-item footer { 44 | padding: 3.2rem 2rem; 45 | background: var(--color-box-footer); 46 | border-top: 1px solid var(--color-line-in-white); 47 | margin-top: 3.2rem; 48 | display: flex; 49 | align-items: center; 50 | justify-content: space-between; 51 | } 52 | 53 | .teacher-item footer p strong { 54 | color: var(--color-primary); 55 | font-size: 1.6rem; 56 | display: block; 57 | } 58 | 59 | .teacher-item footer a { 60 | width: 20rem; 61 | height: 5.6rem; 62 | background: var(--color-secundary); 63 | color: var(--color-button-text); 64 | border: 0; 65 | border-radius: 0.8rem; 66 | cursor: pointer; 67 | font: 700 1.4rem Archivo; 68 | display: flex; 69 | align-items: center; 70 | justify-content: space-evenly; 71 | transition: 0.2s; 72 | text-decoration: none; 73 | } 74 | 75 | .teacher-item footer a:hover { 76 | background: var(--color-secundary-dark); 77 | } 78 | 79 | @media (min-width: 700px) { 80 | .teacher-item header, 81 | .teacher-item footer { 82 | padding: 3.2rem; 83 | } 84 | 85 | .teacher-item > p { 86 | padding: 0 3.2rem; 87 | } 88 | 89 | .teacher-item footer p strong { 90 | display: initial; 91 | margin-left: 1.6rem; 92 | } 93 | 94 | .teacher-item footer a { 95 | width: 24.5rem; 96 | font-size: 1.6rem; 97 | justify-content: center; 98 | } 99 | 100 | .teacher-item footer a img { 101 | margin-right: 1.6rem; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /web/src/components/Textarea/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { TextareaHTMLAttributes } from 'react'; 2 | 3 | import './styles.css'; 4 | 5 | interface TextareaProps extends TextareaHTMLAttributes { 6 | label: string; 7 | name: string; 8 | } 9 | 10 | const Textarea:React.FC = ({ label, name, ...rest }: TextareaProps) => { 11 | return ( 12 |
13 | 14 |