├── .eslintrc.json ├── .gitignore ├── README.md ├── babel.config.js ├── backend ├── helpers.js ├── mock-server.js └── server.js ├── codegrade_mvp.test.js ├── frontend ├── .eslintrc.json ├── components │ ├── App.test.js │ ├── AppClass.js │ └── AppFunctional.js ├── fonts │ ├── OFL.txt │ └── TitilliumWeb-SemiBold.ttf ├── images │ └── Light-Gradient.jpeg ├── index.html ├── index.js └── styles │ ├── reset.css │ └── styles.css ├── index.js ├── jest.config.js ├── jest.globals.js ├── package-lock.json ├── package.json └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true, 6 | "browser": true, 7 | "jest": true 8 | }, 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:react/recommended" 12 | ], 13 | "parserOptions": { 14 | "ecmaVersion": "latest", 15 | "sourceType": "module" 16 | }, 17 | "rules": {} 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | 93 | # Gatsby files 94 | .cache/ 95 | # Comment in the public line in if your project uses Gatsby and not Next.js 96 | # https://nextjs.org/blog/next-9-1#public-directory-support 97 | # public 98 | 99 | # vuepress build output 100 | .vuepress/dist 101 | dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | .vscode 133 | .DS_Store 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sprint Challenge: Advanced React 2 | 3 | ## Intro 4 | 5 | In this challenge, you will write the logic for [THIS WIDGET](https://advanced-react-grid.herokuapp.com/). 6 | 7 | Study its functionality and also inspect the Console, the Network tab and the Elements tab **in Chrome Dev Tools**: 8 | 9 | - There are two versions of the widget with identical functionality: class-based and functional. 10 | - The input box at the bottom of the page expects a valid email address. 11 | - Email validation errors arrive from the server, as you can see in the Network tab, under "Preview". 12 | - The payload sent to the server on form submit can also be seen in the Network tab, under "Payload". 13 | - One valid email in particular, `foo@bar.baz`, **results in a "Forbidden" server error**❗ 14 | - The origin of coordinates of the grid is on its top-left corner. 15 | 16 | ## Requirements 17 | 18 | ### Tools 19 | 20 | - Node >= 18.x 21 | - NPM >= 9.x (update NPM executing `npm i -g npm`) 22 | - Unix-like shell (Gitbash/bash/zsh) 23 | - Chrome >= 100.x 24 | 25 | ❗ Other configurations might work but haven't been tested. 26 | 27 | ## Project Setup 28 | 29 | - Fork, clone, and `npm install`. You won't need to add any extra libraries. 30 | - Launch the project on a development server executing `npm run dev`. 31 | - Visit your widget by navigating Chrome to `http://localhost:3000`. 32 | - Run tests locally executing `npm test`. The test files are `codegrade_mvp.test.js` and `App.test.js`. 33 | 34 | ## API 35 | 36 | - The application includes an endpoint reachable at `POST http://localhost:9000/api/result`. 37 | - You can experiment with this endpoint using an HTTP client like Postman. 38 | - The endpoint expects a payload like `{ "x": 1, "y": 2, "steps": 3, "email": "lady@gaga.com" }`: 39 | - `x` should be an integer between 1 and 3. 40 | - `y` should be an integer between 1 and 3. 41 | - `steps` should be an integer larger than 0. 42 | - `email` should be a valid email address. 43 | - Expect an "Unprocessable Entity" server response if the payload has the wrong shape. 44 | 45 | ## MVP 46 | 47 | ### MVP 1, The Grid 48 | 49 | - Replicate the **functionality and DOM** shown in the prototype linked at the top of this README. 50 | - Keep your code inside `frontend/components/AppFunctional.js`. 51 | - The component exposed by `AppFunctional.js` must be a stateful functional component. 52 | - The component in `AppClass.js` **is optional** but can be tackled if desired. It's not required to pass the sprint. 53 | - The DOM produced by `AppFunctional` must match exactly the DOM in the prototype: 54 | - The hierarchy of HTML elements, their ids, class names etc must be the same. 55 | - The current square is marked with a capital B and an "active" class name. 56 | - The submit success and error messages that display on the page come from the API (see Network tab). 57 | - No frontend form validation code is required. 58 | - The coordinates of each square of the grid are as follows: 59 | 60 | ```js 61 | (1, 1) (2, 1) (3, 1) 62 | (1, 2) (2, 2) (3, 2) 63 | (1, 3) (2, 3) (3, 3) 64 | ``` 65 | 66 | ❗ ALL TESTS MUST PASS 67 | 68 | ### MVP 2, Testing 69 | 70 | - Using `codegrade_mvp.test.js` as inspiration, write 5 tests inside `frontend/components/App.test.js`: 71 | - From inside the test file, import `AppFunctional.js`. 72 | - Test that the visible texts in headings, buttons, links... render on the screen. 73 | - Test that typing on the input results in its value changing to the entered text. 74 | 75 | ### Regarding Grids and Other Notes 76 | 77 | - Inside `AppClass.js` and `AppFunctional` you will find some suggested states and helper functions. Feel free not to use them. 78 | - You don't need a complicated structure to track the state of the grid, because we aren't storing any information in the cells. 79 | - Imagine that the grid were simply a one-dimension array broken --only visually-- into three rows. 80 | - The only component state you need in order to drive the grid is an integer from 0 to 8: **the index the "B" is at.** 81 | - Other pieces of information, like coordinates, can be derived from that index, and don't need a state of their own. 82 | - If you want to make life more complicated (or interesting) for yourself, other structures can be used to store the state of the grid: 83 | 84 | ```js 85 | // A plain array can be used to represent a grid. 86 | // But our App component needn't track the whole array, only the index where the "B" is. 87 | [null, null, null, null, "B", null, null, null, null] 88 | 89 | // 2D arrays or matrices can be used to represent a grid, but this is not recommended in this project: 90 | [[null, null, null], [null, "B", null], [null, null, null]] 91 | 92 | // A string also could work, but strings in JS are immutable making this approach inconvenient: 93 | "xxxxBxxxx" 94 | ``` 95 | 96 | - "Product" works hard designing the messages: we must reproduce them faithfully, down to the last comma. 97 | - ❗ Remember only the functional version is required; the class-based one is recommended but optional. 98 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const PLUGIN_TRANSFORM_RUNTIME = '@babel/plugin-transform-runtime' 2 | const PLUGIN_STYLED_COMPONENTS = 'babel-plugin-styled-components' 3 | 4 | const PRESET_REACT = '@babel/preset-react' 5 | const PRESET_ENV = '@babel/preset-env' 6 | 7 | module.exports = { 8 | env: { 9 | testing: { 10 | plugins: [ 11 | [PLUGIN_TRANSFORM_RUNTIME], 12 | ], 13 | presets: [ 14 | [PRESET_REACT], 15 | [PRESET_ENV, { modules: 'commonjs', debug: false }] 16 | ] 17 | }, 18 | development: { 19 | plugins: [ 20 | [PLUGIN_STYLED_COMPONENTS], 21 | ], 22 | presets: [ 23 | [PRESET_REACT], 24 | [PRESET_ENV, { targets: { chrome: '96' } }] 25 | ] 26 | }, 27 | production: { 28 | plugins: [ 29 | [PLUGIN_STYLED_COMPONENTS], 30 | ], 31 | presets: [ 32 | [PRESET_REACT], 33 | [PRESET_ENV, { targets: { chrome: '96' } }] 34 | ] 35 | }, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/helpers.js: -------------------------------------------------------------------------------- 1 | const yup = require('yup') 2 | 3 | const schema = yup.object().shape({ 4 | email: yup 5 | .string() 6 | .trim() 7 | .email('email must be a valid email') 8 | .required('email is required') 9 | .max(100, 'email must be under 100 chars'), 10 | x: yup 11 | .number() 12 | .typeError('x coordinate must be a number') 13 | .required('x coordinate is required') 14 | .max(3, 'x coordinate must be 1, 2 or 3') 15 | .min(1, 'x coordinate must be 1, 2 or 3'), 16 | y: yup 17 | .number() 18 | .typeError('y coordinate must be a number') 19 | .required('y coordinate is required') 20 | .max(3, 'y coordinate must be 1, 2 or 3') 21 | .min(1, 'y coordinate must be 1, 2 or 3'), 22 | steps: yup 23 | .number() 24 | .typeError('steps must be a number') 25 | .required('steps is required') 26 | .min(0, 'steps must be 0 or greater'), 27 | }) 28 | 29 | async function buildResponse(req) { 30 | let status = 200 31 | let message 32 | 33 | try { 34 | const validated = await schema.validate(req.body, { stripUnknown: true }) 35 | 36 | const { email, x, y, steps } = validated 37 | const code = (((x + 1) * (y + 2)) * (steps + 1)) + email.length 38 | 39 | if (email === 'foo@bar.baz') { 40 | message = `foo@bar.baz failure #${code}` 41 | status = 403 42 | } else { 43 | const name = email.split('@')[0] 44 | message = `${name} win #${code}` 45 | } 46 | } catch (err) { 47 | message = `Ouch: ${err.message}` 48 | status = 422 49 | } 50 | 51 | return [status, { message }] 52 | } 53 | 54 | module.exports = { 55 | buildResponse, 56 | } 57 | -------------------------------------------------------------------------------- /backend/mock-server.js: -------------------------------------------------------------------------------- 1 | const { setupServer } = require('msw/node') 2 | const { rest } = require('msw') 3 | 4 | const { buildResponse } = require('./helpers') 5 | 6 | async function result(req, res, ctx) { 7 | const [status, payload] = await buildResponse(req) 8 | return res( 9 | ctx.status(status), 10 | ctx.json(payload), 11 | ) 12 | } 13 | 14 | function catchAll(req, res, ctx) { 15 | const message = `Endpoint [${req.method}] /${req.params['0']} does not exist` 16 | return res( 17 | ctx.status(404), 18 | ctx.json({ message }), 19 | ) 20 | } 21 | 22 | const handlers = [ 23 | rest.post('http://localhost:9000/api/result', result), 24 | rest.all('http://localhost:9000/*', catchAll), 25 | ] 26 | 27 | module.exports = setupServer(...handlers) 28 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const cors = require('cors') 3 | const path = require('path') 4 | 5 | const { buildResponse } = require('./helpers') 6 | 7 | const server = express() 8 | 9 | server.use(express.json()) 10 | 11 | server.use(express.static(path.join(__dirname, '../dist'))) 12 | 13 | server.use(cors()) 14 | 15 | server.post('/api/result', async (req, res) => { 16 | const [status, payload] = await buildResponse(req) 17 | res.status(status).json(payload) 18 | }) 19 | 20 | server.get('*', (req, res) => { 21 | res.sendFile(path.join(__dirname, '../dist/index.html')) 22 | }) 23 | 24 | server.use((req, res) => { 25 | res.status(404).json({ 26 | message: `Endpoint [${req.method}] ${req.originalUrl} does not exist`, 27 | }) 28 | }) 29 | 30 | module.exports = server 31 | -------------------------------------------------------------------------------- /codegrade_mvp.test.js: -------------------------------------------------------------------------------- 1 | import server from './backend/mock-server' 2 | import React from 'react' 3 | import AppFunctional from './frontend/components/AppFunctional' 4 | // ❗ class component is optional, uncomment next line to test 5 | // import AppClass from './frontend/components/AppClass' 6 | import { render, fireEvent, screen } from '@testing-library/react' 7 | import '@testing-library/jest-dom/extend-expect' 8 | 9 | jest.setTimeout(1000) // default 5000 too long for Codegrade 10 | const waitForOptions = { timeout: 100 } 11 | const queryOptions = { exact: false } 12 | 13 | let up, down, left, right, reset, submit 14 | let squares, coordinates, steps, message, email 15 | 16 | const updateStatelessSelectors = document => { 17 | up = document.querySelector('#up') 18 | down = document.querySelector('#down') 19 | left = document.querySelector('#left') 20 | right = document.querySelector('#right') 21 | reset = document.querySelector('#reset') 22 | submit = document.querySelector('#submit') 23 | } 24 | 25 | const updateStatefulSelectors = document => { 26 | squares = document.querySelectorAll('.square') 27 | coordinates = document.querySelector('#coordinates') 28 | steps = document.querySelector('#steps') 29 | message = document.querySelector('#message') 30 | email = document.querySelector('#email') 31 | } 32 | 33 | const testSquares = (squares, activeIdx) => { 34 | squares.forEach((square, idx) => { 35 | if (idx === activeIdx) { 36 | expect(square.textContent).toBe('B') 37 | expect(square.className).toMatch(/active/) 38 | } else { 39 | expect(square.textContent).toBeFalsy() 40 | expect(square.className).not.toMatch(/active/) 41 | } 42 | }) 43 | } 44 | 45 | test('AppFunctional is a functional component', () => { 46 | expect( 47 | AppFunctional.prototype && 48 | AppFunctional.prototype.isReactComponent 49 | ).not.toBeTruthy() 50 | }); 51 | // ❗ class component is optional, uncomment next lines to test 52 | // test('AppClass is a class-based component', () => { 53 | // expect( 54 | // AppClass.prototype && 55 | // AppClass.prototype.isReactComponent 56 | // ).toBeTruthy() 57 | // }); 58 | 59 | // ❗ class component is optional, uncomment AppClass to test 60 | [AppFunctional, /* AppClass */].forEach((Component, idx) => { 61 | const label = idx === 0 ? 'FUNCTIONAL' : 'CLASS-BASED' 62 | 63 | describe(`${label}`, () => { 64 | beforeAll(() => { server.listen() }) 65 | afterAll(() => { server.close() }) 66 | beforeEach(() => { 67 | render() 68 | updateStatelessSelectors(document) 69 | updateStatefulSelectors(document) 70 | }) 71 | afterEach(() => { 72 | server.resetHandlers() 73 | document.body.innerHTML = '' 74 | }) 75 | 76 | describe(`[A ${label}] Active Square`, () => { 77 | test(`[A1 ${label}] Actions: none (Initial State of ) 78 | Active Square should be index 4`, () => { 79 | testSquares(squares, 4) 80 | }) 81 | test(`[A2 ${label}] Actions: up 82 | Active Square should be index 1`, () => { 83 | fireEvent.click(up) 84 | testSquares(squares, 1) 85 | }) 86 | test(`[A3 ${label}] Actions: up, up 87 | Active Square should be index 1`, () => { 88 | fireEvent.click(up) 89 | fireEvent.click(up) 90 | testSquares(squares, 1) 91 | }) 92 | test(`[A4 ${label}] Actions: up, left 93 | Active Square should be index 0`, () => { 94 | fireEvent.click(up) 95 | fireEvent.click(left) 96 | testSquares(squares, 0) 97 | }) 98 | test(`[A5 ${label}] Actions: up, left, left 99 | Active Square should be index 0`, () => { 100 | fireEvent.click(up) 101 | fireEvent.click(left) 102 | fireEvent.click(left) 103 | testSquares(squares, 0) 104 | }) 105 | test(`[A6 ${label}] Actions: up, right 106 | Active Square should be index 2`, () => { 107 | fireEvent.click(up) 108 | fireEvent.click(right) 109 | testSquares(squares, 2) 110 | }) 111 | test(`[A7 ${label}] Actions: up, right, right 112 | Active Square should be index 2`, () => { 113 | fireEvent.click(up) 114 | fireEvent.click(right) 115 | fireEvent.click(right) 116 | testSquares(squares, 2) 117 | }) 118 | test(`[A8 ${label}] Actions: right 119 | Active Square should be index 5`, () => { 120 | fireEvent.click(right) 121 | testSquares(squares, 5) 122 | }) 123 | test(`[A9 ${label}] Actions: right, right 124 | Active Square should be index 5`, () => { 125 | fireEvent.click(right) 126 | fireEvent.click(right) 127 | testSquares(squares, 5) 128 | }) 129 | test(`[A10 ${label}] Actions: right, down 130 | Active Square should be index 8`, () => { 131 | fireEvent.click(right) 132 | fireEvent.click(down) 133 | testSquares(squares, 8) 134 | }) 135 | test(`[A11 ${label}] Actions: right, down, down 136 | Active Square should be index 8`, () => { 137 | fireEvent.click(right) 138 | fireEvent.click(down) 139 | fireEvent.click(down) 140 | testSquares(squares, 8) 141 | }) 142 | test(`[A12 ${label}] Actions: down, left 143 | Active Square should be index 6`, () => { 144 | fireEvent.click(down) 145 | fireEvent.click(left) 146 | testSquares(squares, 6) 147 | }) 148 | test(`[A13 ${label}] Actions: down, down, left, left 149 | Active Square should be index 6`, () => { 150 | fireEvent.click(down) 151 | fireEvent.click(down) 152 | fireEvent.click(left) 153 | fireEvent.click(left) 154 | testSquares(squares, 6) 155 | }) 156 | }) 157 | describe(`[B ${label}] Coordinates Readout`, () => { 158 | test(`[B1] Actions: none (Initial State of ) 159 | Coordinates should be (2,2)`, () => { 160 | expect(coordinates.textContent).toMatch(/\(2.*2\)$/) 161 | }) 162 | test(`[B2 ${label}] Actions: up 163 | Coordinates should be (2,1)`, () => { 164 | fireEvent.click(up) 165 | expect(coordinates.textContent).toMatch(/\(2.*1\)$/) 166 | }) 167 | test(`[B3 ${label}] Actions: up, up 168 | Coordinates should be (2,1)`, () => { 169 | fireEvent.click(up) 170 | fireEvent.click(up) 171 | expect(coordinates.textContent).toMatch(/\(2.*1\)$/) 172 | }) 173 | test(`[B4 ${label}] Actions: up, left 174 | Coordinates should be (1,1)`, () => { 175 | fireEvent.click(up) 176 | fireEvent.click(left) 177 | expect(coordinates.textContent).toMatch(/\(1.*1\)$/) 178 | }) 179 | test(`[B5 ${label}] Actions: up, left, left 180 | Coordinates should be (1,1)`, () => { 181 | fireEvent.click(up) 182 | fireEvent.click(left) 183 | fireEvent.click(left) 184 | expect(coordinates.textContent).toMatch(/\(1.*1\)$/) 185 | }) 186 | test(`[B6 ${label}] Actions: up, right 187 | Coordinates should be (3,1)`, () => { 188 | fireEvent.click(up) 189 | fireEvent.click(right) 190 | expect(coordinates.textContent).toMatch(/\(3.*1\)$/) 191 | }) 192 | test(`[B7 ${label}] Actions: up, right, right 193 | Coordinates should be (3,1)`, () => { 194 | fireEvent.click(up) 195 | fireEvent.click(right) 196 | fireEvent.click(right) 197 | expect(coordinates.textContent).toMatch(/\(3.*1\)$/) 198 | }) 199 | test(`[B8 ${label}] Actions: right 200 | Coordinates should be (3,2)`, () => { 201 | fireEvent.click(right) 202 | expect(coordinates.textContent).toMatch(/\(3.*2\)$/) 203 | }) 204 | test(`[B9 ${label}] Actions: right, right 205 | Coordinates should be (3,2)`, () => { 206 | fireEvent.click(right) 207 | fireEvent.click(right) 208 | expect(coordinates.textContent).toMatch(/\(3.*2\)$/) 209 | }) 210 | test(`[B10 ${label}] Actions: right, down 211 | Coordinates should be (3,3)`, () => { 212 | fireEvent.click(right) 213 | fireEvent.click(down) 214 | expect(coordinates.textContent).toMatch(/\(3.*3\)$/) 215 | }) 216 | test(`[B11 ${label}] Actions: right, down, down 217 | Coordinates should be (3,3)`, () => { 218 | fireEvent.click(right) 219 | fireEvent.click(down) 220 | fireEvent.click(down) 221 | expect(coordinates.textContent).toMatch(/\(3.*3\)$/) 222 | }) 223 | test(`[B12 ${label}] Actions: down, left 224 | Coordinates should be (1,3)`, () => { 225 | fireEvent.click(down) 226 | fireEvent.click(left) 227 | expect(coordinates.textContent).toMatch(/\(1.*3\)$/) 228 | }) 229 | test(`[B13 ${label}] Actions: down, down, left, left 230 | Coordinates should be (1,3)`, () => { 231 | fireEvent.click(down) 232 | fireEvent.click(down) 233 | fireEvent.click(left) 234 | fireEvent.click(left) 235 | expect(coordinates.textContent).toMatch(/\(1.*3\)$/) 236 | }) 237 | }) 238 | describe(`[C ${label}] Limit Reached Message`, () => { 239 | test(`[C1 ${label}] Actions: none (Initial State of ) 240 | Limit reached message should be empty`, () => { 241 | expect(message.textContent).toBeFalsy() 242 | }) 243 | test(`[C2 ${label}] Actions: up 244 | Limit reached message should be empty`, () => { 245 | fireEvent.click(up) 246 | expect(message.textContent).toBeFalsy() 247 | }) 248 | test(`[C3 ${label}] Actions: up, up 249 | Limit reached message should be "You can't go up"`, () => { 250 | fireEvent.click(up) 251 | fireEvent.click(up) 252 | expect(message.textContent).toBe("You can't go up") 253 | }) 254 | test(`[C4 ${label}] Actions: up, left 255 | Limit reached message should be empty`, () => { 256 | fireEvent.click(up) 257 | fireEvent.click(left) 258 | expect(message.textContent).toBeFalsy() 259 | }) 260 | test(`[C5 ${label}] Actions: up, left, left 261 | Limit reached message should be "You can't go left"`, () => { 262 | fireEvent.click(up) 263 | fireEvent.click(left) 264 | fireEvent.click(left) 265 | expect(message.textContent).toBe("You can't go left") 266 | }) 267 | test(`[C6 ${label}] Actions: up, right 268 | Limit reached message should be empty`, () => { 269 | fireEvent.click(up) 270 | fireEvent.click(right) 271 | expect(message.textContent).toBeFalsy() 272 | }) 273 | test(`[C7 ${label}] Actions: up, right, right 274 | Limit reached message should be "You can't go right"`, () => { 275 | fireEvent.click(up) 276 | fireEvent.click(right) 277 | fireEvent.click(right) 278 | expect(message.textContent).toBe("You can't go right") 279 | }) 280 | test(`[C8 ${label}] Actions: right 281 | Limit reached message should be empty`, () => { 282 | fireEvent.click(right) 283 | expect(message.textContent).toBeFalsy() 284 | }) 285 | test(`[C9 ${label}] Actions: right, right 286 | Limit reached message should be (3,2)`, () => { 287 | fireEvent.click(right) 288 | fireEvent.click(right) 289 | expect(message.textContent).toBe("You can't go right") 290 | }) 291 | test(`[C10 ${label}] Actions: right, down 292 | Limit reached message should be empty`, () => { 293 | fireEvent.click(right) 294 | fireEvent.click(down) 295 | expect(message.textContent).toBeFalsy() 296 | }) 297 | test(`[C11 ${label}] Actions: right, down, down 298 | Limit reached message should be "You can't go down"`, () => { 299 | fireEvent.click(right) 300 | fireEvent.click(down) 301 | fireEvent.click(down) 302 | expect(message.textContent).toBe("You can't go down") 303 | }) 304 | test(`[C12 ${label}] Actions: down, left 305 | Limit reached message should be empty`, () => { 306 | fireEvent.click(down) 307 | fireEvent.click(left) 308 | expect(message.textContent).toBeFalsy() 309 | }) 310 | test(`[C13 ${label}] Actions: down, down, left, left 311 | Limit reached message should be "You can't go left"`, () => { 312 | fireEvent.click(down) 313 | fireEvent.click(down) 314 | fireEvent.click(left) 315 | fireEvent.click(left) 316 | expect(message.textContent).toBe("You can't go left") 317 | }) 318 | }) 319 | describe(`[D ${label}] Steps Counter`, () => { 320 | test(`[D1 ${label}] Steps counter works correctly`, () => { 321 | expect(steps.textContent).toBe("You moved 0 times") 322 | fireEvent.click(up) 323 | fireEvent.click(up) 324 | fireEvent.click(left) 325 | expect(steps.textContent).toBe("You moved 2 times") 326 | fireEvent.click(right) 327 | fireEvent.click(right) 328 | expect(steps.textContent).toBe("You moved 4 times") 329 | fireEvent.click(down) 330 | fireEvent.click(down) 331 | fireEvent.click(down) 332 | expect(steps.textContent).toBe("You moved 6 times") 333 | fireEvent.click(left) 334 | fireEvent.click(left) 335 | fireEvent.click(left) 336 | expect(steps.textContent).toBe("You moved 8 times") 337 | }) 338 | test(`[D2 ${label}] Steps counter handles a single step gracefully`, () => { 339 | fireEvent.click(up) 340 | expect(steps.textContent).toBe("You moved 1 time") 341 | fireEvent.click(up) 342 | expect(steps.textContent).toBe("You moved 1 time") 343 | }) 344 | }) 345 | describe(`[E ${label}] Reset Button`, () => { 346 | test(`[E1 ${label}] Active Square is reset`, () => { 347 | fireEvent.click(up) 348 | fireEvent.click(up) 349 | fireEvent.click(left) 350 | testSquares(squares, 0) 351 | fireEvent.click(reset) 352 | testSquares(squares, 4) 353 | }) 354 | test(`[E2 ${label}] Coordinates are reset`, () => { 355 | fireEvent.click(up) 356 | fireEvent.click(up) 357 | fireEvent.click(left) 358 | expect(coordinates.textContent).toMatch(/\(1.*1\)$/) 359 | fireEvent.click(reset) 360 | expect(coordinates.textContent).toMatch(/\(2.*2\)$/) 361 | }) 362 | test(`[E3 ${label}] Message is reset`, () => { 363 | fireEvent.click(up) 364 | fireEvent.click(up) 365 | expect(message.textContent).toBe("You can't go up") 366 | fireEvent.click(reset) 367 | expect(message.textContent).toBeFalsy() 368 | }) 369 | test(`[E4 ${label}] Steps are reset`, () => { 370 | fireEvent.click(up) 371 | fireEvent.click(up) 372 | fireEvent.click(left) 373 | expect(steps.textContent).toBe("You moved 2 times") 374 | fireEvent.click(reset) 375 | expect(steps.textContent).toBe("You moved 0 times") 376 | }) 377 | test(`[E5 ${label}] Email input is reset`, () => { 378 | fireEvent.change(email, { target: { value: 'lady@gaga.com' } }) 379 | expect(email).toHaveValue('lady@gaga.com') 380 | fireEvent.click(reset) 381 | expect(email.value).toBeFalsy() 382 | }) 383 | }) 384 | describe(`[F ${label}] Submit Button`, () => { 385 | test(`[F1 ${label}] Actions: up, type email, submit 386 | Success message is correct`, async () => { 387 | fireEvent.click(up) 388 | fireEvent.change(email, { target: { value: 'lady@gaga.com' } }) 389 | fireEvent.click(submit) 390 | await screen.findByText('lady win #31', queryOptions, waitForOptions) 391 | }) 392 | test(`[F2 ${label}] Actions: down, down, type email, submit 393 | Success message is correct`, async () => { 394 | fireEvent.click(down) 395 | fireEvent.click(down) 396 | fireEvent.change(email, { target: { value: 'lady@gaga.com' } }) 397 | fireEvent.click(submit) 398 | await screen.findByText('lady win #43', queryOptions, waitForOptions) 399 | }) 400 | test(`[F3 ${label}] Actions: up, down, left, right, type email, submit 401 | Success message is correct`, async () => { 402 | fireEvent.click(up) 403 | fireEvent.click(down) 404 | fireEvent.click(left) 405 | fireEvent.click(right) 406 | fireEvent.change(email, { target: { value: 'lady@gaga.com' } }) 407 | fireEvent.click(submit) 408 | await screen.findByText('lady win #73', queryOptions, waitForOptions) 409 | }) 410 | test(`[F4 ${label}] Actions: down, right, submit 411 | Error message on no email is correct`, async () => { 412 | fireEvent.click(down) 413 | fireEvent.click(right) 414 | fireEvent.click(submit) 415 | await screen.findByText('Ouch: email is required', queryOptions, waitForOptions) 416 | }) 417 | test(`[F5 ${label}] Actions: down, right, type invalid email, submit 418 | Error message on invalid email is correct`, async () => { 419 | fireEvent.click(down) 420 | fireEvent.click(right) 421 | fireEvent.change(email, { target: { value: 'bad@email' } }) 422 | fireEvent.click(submit) 423 | await screen.findByText('Ouch: email must be a valid email', queryOptions, waitForOptions) 424 | }) 425 | test(`[F6 ${label}] Actions: down, right, type foo@bar.baz email, submit 426 | Error message on banned email is correct`, async () => { 427 | fireEvent.click(down) 428 | fireEvent.click(right) 429 | fireEvent.change(email, { target: { value: 'foo@bar.baz' } }) 430 | fireEvent.click(submit) 431 | await screen.findByText('foo@bar.baz failure #71', queryOptions, waitForOptions) 432 | }) 433 | test(`[F7 ${label}] Actions: left, type valid email, submit 434 | Submitting resets the email input`, async () => { 435 | fireEvent.click(left) 436 | fireEvent.change(email, { target: { value: 'lady@gaga.com' } }) 437 | fireEvent.click(submit) 438 | await screen.findByText('lady win #29', queryOptions, waitForOptions) 439 | expect(email.value).toBeFalsy() 440 | }) 441 | test(`[F8 ${label}] Actions: up, right, type valid email, submit 442 | Submitting does not reset coordinates nor steps`, async () => { 443 | fireEvent.click(up) 444 | fireEvent.click(right) 445 | fireEvent.change(email, { target: { value: 'lady@gaga.com' } }) 446 | fireEvent.click(submit) 447 | await screen.findByText('lady win #49', queryOptions, waitForOptions) 448 | expect(coordinates.textContent).toMatch(/\(3.*1\)$/) 449 | expect(steps.textContent).toBe('You moved 2 times') 450 | }) 451 | }) 452 | }) 453 | }) 454 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:react/recommended" 11 | ], 12 | "parserOptions": { 13 | "ecmaFeatures": { 14 | "jsx": true 15 | }, 16 | "ecmaVersion": "latest", 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "react" 21 | ], 22 | "rules": { 23 | "react/prop-types": 0 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/components/App.test.js: -------------------------------------------------------------------------------- 1 | // Write your tests here 2 | test('sanity', () => { 3 | expect(true).toBe(false) 4 | }) 5 | -------------------------------------------------------------------------------- /frontend/components/AppClass.js: -------------------------------------------------------------------------------- 1 | // ❗ OPTIONAL, not required to pass the sprint 2 | // ❗ OPTIONAL, not required to pass the sprint 3 | // ❗ OPTIONAL, not required to pass the sprint 4 | import React from 'react' 5 | 6 | // Suggested initial states 7 | const initialMessage = '' 8 | const initialEmail = '' 9 | const initialSteps = 0 10 | const initialIndex = 4 // the index the "B" is at 11 | 12 | const initialState = { 13 | message: initialMessage, 14 | email: initialEmail, 15 | index: initialIndex, 16 | steps: initialSteps, 17 | } 18 | 19 | export default class AppClass extends React.Component { 20 | // THE FOLLOWING HELPERS ARE JUST RECOMMENDATIONS. 21 | // You can delete them and build your own logic from scratch. 22 | 23 | getXY = () => { 24 | // It it not necessary to have a state to track the coordinates. 25 | // It's enough to know what index the "B" is at, to be able to calculate them. 26 | } 27 | 28 | getXYMessage = () => { 29 | // It it not necessary to have a state to track the "Coordinates (2, 2)" message for the user. 30 | // You can use the `getXY` helper above to obtain the coordinates, and then `getXYMessage` 31 | // returns the fully constructed string. 32 | } 33 | 34 | reset = () => { 35 | // Use this helper to reset all states to their initial values. 36 | } 37 | 38 | getNextIndex = (direction) => { 39 | // This helper takes a direction ("left", "up", etc) and calculates what the next index 40 | // of the "B" would be. If the move is impossible because we are at the edge of the grid, 41 | // this helper should return the current index unchanged. 42 | } 43 | 44 | move = (evt) => { 45 | // This event handler can use the helper above to obtain a new index for the "B", 46 | // and change any states accordingly. 47 | } 48 | 49 | onChange = (evt) => { 50 | // You will need this to update the value of the input. 51 | } 52 | 53 | onSubmit = (evt) => { 54 | // Use a POST request to send a payload to the server. 55 | } 56 | 57 | render() { 58 | const { className } = this.props 59 | return ( 60 |
61 |

(This component is not required to pass the sprint)

62 |
63 |

Coordinates (2, 2)

64 |

You moved 0 times

65 |
66 |
67 | { 68 | [0, 1, 2, 3, 4, 5, 6, 7, 8].map(idx => ( 69 |
70 | {idx === 4 ? 'B' : null} 71 |
72 | )) 73 | } 74 |
75 |
76 |

77 |
78 |
79 | 80 | 81 | 82 | 83 | 84 |
85 |
86 | 87 | 88 |
89 |
90 | ) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /frontend/components/AppFunctional.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // Suggested initial states 4 | const initialMessage = '' 5 | const initialEmail = '' 6 | const initialSteps = 0 7 | const initialIndex = 4 // the index the "B" is at 8 | 9 | export default function AppFunctional(props) { 10 | // THE FOLLOWING HELPERS ARE JUST RECOMMENDATIONS. 11 | // You can delete them and build your own logic from scratch. 12 | 13 | function getXY() { 14 | // It it not necessary to have a state to track the coordinates. 15 | // It's enough to know what index the "B" is at, to be able to calculate them. 16 | } 17 | 18 | function getXYMessage() { 19 | // It it not necessary to have a state to track the "Coordinates (2, 2)" message for the user. 20 | // You can use the `getXY` helper above to obtain the coordinates, and then `getXYMessage` 21 | // returns the fully constructed string. 22 | } 23 | 24 | function reset() { 25 | // Use this helper to reset all states to their initial values. 26 | } 27 | 28 | function getNextIndex(direction) { 29 | // This helper takes a direction ("left", "up", etc) and calculates what the next index 30 | // of the "B" would be. If the move is impossible because we are at the edge of the grid, 31 | // this helper should return the current index unchanged. 32 | } 33 | 34 | function move(evt) { 35 | // This event handler can use the helper above to obtain a new index for the "B", 36 | // and change any states accordingly. 37 | } 38 | 39 | function onChange(evt) { 40 | // You will need this to update the value of the input. 41 | } 42 | 43 | function onSubmit(evt) { 44 | // Use a POST request to send a payload to the server. 45 | } 46 | 47 | return ( 48 |
49 |
50 |

Coordinates (2, 2)

51 |

You moved 0 times

52 |
53 |
54 | { 55 | [0, 1, 2, 3, 4, 5, 6, 7, 8].map(idx => ( 56 |
57 | {idx === 4 ? 'B' : null} 58 |
59 | )) 60 | } 61 |
62 |
63 |

64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 |
72 |
73 | 74 | 75 |
76 |
77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /frontend/fonts/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2011 by Accademia di Belle Arti di Urbino and students of MA course of Visual design. Some rights reserved. 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /frontend/fonts/TitilliumWeb-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloominstituteoftechnology/web-sprint-challenge-advanced-react/3b22e6f6c0b76bcafc783f0de1dd8c6ab23f2d1b/frontend/fonts/TitilliumWeb-SemiBold.ttf -------------------------------------------------------------------------------- /frontend/images/Light-Gradient.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloominstituteoftechnology/web-sprint-challenge-advanced-react/3b22e6f6c0b76bcafc783f0de1dd8c6ab23f2d1b/frontend/images/Light-Gradient.jpeg -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | Grid 14 | 15 | 16 | 17 |
18 |
Bloom Institute of Technology 2024
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /frontend/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { BrowserRouter, NavLink, Routes, Route } from 'react-router-dom' 4 | import AppClass from './components/AppClass' 5 | import AppFunctional from './components/AppFunctional' 6 | import './styles/reset.css' 7 | import './styles/styles.css' 8 | 9 | const container = document.getElementById('root') 10 | const root = createRoot(container) 11 | 12 | root.render( 13 | 14 |

Welcome to the GRID

15 | 19 | 20 | } /> 21 | } /> 22 | 23 |
24 | ) 25 | -------------------------------------------------------------------------------- /frontend/styles/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | 27 | /* HTML5 display-role reset for older browsers */ 28 | article, aside, details, figcaption, figure, 29 | footer, header, hgroup, menu, nav, section { 30 | display: block; 31 | } 32 | 33 | body { 34 | line-height: 1; 35 | } 36 | 37 | ol, ul { 38 | list-style: none; 39 | } 40 | 41 | blockquote, q { 42 | quotes: none; 43 | } 44 | 45 | blockquote:before, blockquote:after, 46 | q:before, q:after { 47 | content: ''; 48 | content: none; 49 | } 50 | 51 | table { 52 | border-collapse: collapse; 53 | border-spacing: 0; 54 | } 55 | 56 | html { 57 | box-sizing: border-box; 58 | } 59 | 60 | *, *:before, *:after { 61 | box-sizing: inherit; 62 | } 63 | -------------------------------------------------------------------------------- /frontend/styles/styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Titillium Web'; 3 | src: url('../fonts/TitilliumWeb-SemiBold.ttf'); 4 | } 5 | 6 | html, body { 7 | min-height:100vh; 8 | } 9 | 10 | body::after { 11 | content: ""; 12 | background: url('../images/Light-Gradient.jpeg'); 13 | opacity: 0.3; 14 | top: 0; 15 | left: 0; 16 | bottom: 0; 17 | right: 0; 18 | position: absolute; 19 | z-index: -1; 20 | } 21 | 22 | body, input, button { 23 | font-family: 'Titillium Web', sans-serif; 24 | color: #484848; 25 | } 26 | 27 | #root, #wrapper { 28 | display: flex; 29 | flex-direction: column; 30 | align-items: center; 31 | } 32 | 33 | h1 { 34 | font-size: 1.8rem; 35 | line-height: 4rem; 36 | color: #93005a; 37 | } 38 | 39 | nav a { 40 | line-height: 3.2rem; 41 | padding-left: 1rem; 42 | padding-right: 1rem; 43 | color: #484848; 44 | } 45 | 46 | nav a.active:nth-last-of-type(1) { 47 | color: #00808c; 48 | } 49 | 50 | nav a.active:nth-last-of-type(2) { 51 | color: #ff4b00; 52 | } 53 | 54 | .info { 55 | width: 100%; 56 | display: flex; 57 | flex-direction: column; 58 | align-items: center; 59 | justify-content: space-evenly; 60 | min-height: 3.8rem; 61 | background-color: rgb(252, 252, 252); 62 | border: 1px solid rgb(238, 238, 238); 63 | margin: 1rem 0 1rem 0; 64 | } 65 | 66 | #coordinates, #steps { 67 | font-size: 1.2rem; 68 | } 69 | 70 | #message { 71 | font-size: 1.2rem; 72 | color: #00808c; 73 | font-style: italic; 74 | } 75 | 76 | .class-based #message { 77 | color: #ff4b00; 78 | } 79 | 80 | #grid { 81 | display: grid; 82 | grid-template-columns: 100px 100px 100px; 83 | grid-template-rows: 100px 100px 100px; 84 | } 85 | 86 | #grid .square { 87 | border: 4px solid black; 88 | margin: -2px; 89 | display: flex; 90 | align-items: center; 91 | justify-content: center; 92 | font-size: 4rem; 93 | color: white; 94 | } 95 | 96 | .class-based #grid .square, .functional #grid .square.active { 97 | background-color: #00808c 98 | } 99 | 100 | .class-based #grid .square.active, .functional #grid .square { 101 | background-color: #ff4b00; 102 | } 103 | 104 | #keypad { 105 | background-color: rgb(252, 252, 252); 106 | border-radius: 50%; 107 | border: 1px solid rgb(238, 238, 238); 108 | padding: 0.55rem; 109 | margin-bottom: 1rem; 110 | display: grid; 111 | grid-template-columns: 55px 55px 55px; 112 | grid-template-rows: 55px 55px 55px; 113 | grid-template-areas: 114 | ". up ." 115 | "left reset right" 116 | ". down ."; 117 | } 118 | 119 | #keypad button { 120 | border: 1px solid #DDDDDD; 121 | border-radius: 28%; 122 | align-self: center; 123 | justify-self: center; 124 | width: 55px; 125 | height: 55px; 126 | } 127 | 128 | #keypad button:active { 129 | box-shadow: 0 0 5px #93005a; 130 | border: 1px solid #93005a; 131 | background-color: white; 132 | } 133 | 134 | button#reset { 135 | width: 80%; 136 | height: 80%; 137 | border-radius: 50%; 138 | grid-area: reset; 139 | } 140 | 141 | button#left { 142 | grid-area: left; 143 | } 144 | 145 | button#up { 146 | grid-area: up; 147 | } 148 | 149 | button#right { 150 | grid-area: right; 151 | } 152 | 153 | button#down { 154 | grid-area: down; 155 | } 156 | 157 | input { 158 | height: 1.9rem; 159 | border-radius: 0; 160 | margin: -1px; 161 | border: 1px solid #DDDDDD; 162 | background-color: white; 163 | outline: none; 164 | } 165 | 166 | input[type=email] { 167 | width: 230px; 168 | margin-right: 1rem; 169 | } 170 | 171 | input:active { 172 | box-shadow: 0 0 5px #93005a; 173 | border: 1px solid #93005a; 174 | } 175 | 176 | footer { 177 | margin-top: 2rem; 178 | line-height: 2rem; 179 | text-align: center; 180 | color: #93005a; 181 | } 182 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const server = require('./backend/server') 2 | 3 | const PORT = process.env.PORT || 9000 4 | 5 | server.listen(PORT, () => { 6 | console.log(`listening on ${PORT}`) 7 | }) 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | module.exports = { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/mq/wc13nlwj4gdgcyshdy5j1hgr0000gn/T/jest_dx", 15 | 16 | // Automatically clear mock calls, instances, contexts and results before every test 17 | // clearMocks: false, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: false, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | // coverageDirectory: undefined, 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | coverageProvider: "v8", 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // The default configuration for fake timers 54 | // fakeTimers: { 55 | // "enableGlobally": false 56 | // }, 57 | 58 | // Force coverage collection from ignored files using an array of glob patterns 59 | // forceCoverageMatch: [], 60 | 61 | // A path to a module which exports an async function that is triggered once before all test suites 62 | // globalSetup: undefined, 63 | 64 | // A path to a module which exports an async function that is triggered once after all test suites 65 | // globalTeardown: undefined, 66 | 67 | // A set of global variables that need to be available in all test environments 68 | // globals: {}, 69 | 70 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 71 | // maxWorkers: "50%", 72 | 73 | // An array of directory names to be searched recursively up from the requiring module's location 74 | // moduleDirectories: [ 75 | // "node_modules" 76 | // ], 77 | 78 | // An array of file extensions your modules use 79 | // moduleFileExtensions: [ 80 | // "js", 81 | // "mjs", 82 | // "cjs", 83 | // "jsx", 84 | // "ts", 85 | // "tsx", 86 | // "json", 87 | // "node" 88 | // ], 89 | 90 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 91 | // moduleNameMapper: {}, 92 | 93 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 94 | // modulePathIgnorePatterns: [], 95 | 96 | // Activates notifications for test results 97 | // notify: false, 98 | 99 | // An enum that specifies notification mode. Requires { notify: true } 100 | // notifyMode: "failure-change", 101 | 102 | // A preset that is used as a base for Jest's configuration 103 | // preset: undefined, 104 | 105 | // Run tests from one or more projects 106 | // projects: undefined, 107 | 108 | // Use this configuration option to add custom reporters to Jest 109 | // reporters: undefined, 110 | 111 | // Automatically reset mock state before every test 112 | // resetMocks: false, 113 | 114 | // Reset the module registry before running each individual test 115 | // resetModules: false, 116 | 117 | // A path to a custom resolver 118 | // resolver: undefined, 119 | 120 | // Automatically restore mock state and implementation before every test 121 | // restoreMocks: false, 122 | 123 | // The root directory that Jest should scan for tests and modules within 124 | // rootDir: undefined, 125 | 126 | // A list of paths to directories that Jest should use to search for files in 127 | // roots: [ 128 | // "" 129 | // ], 130 | 131 | // Allows you to use a custom runner instead of Jest's default test runner 132 | // runner: "jest-runner", 133 | 134 | // The paths to modules that run some code to configure or set up the testing environment before each test 135 | "setupFiles": [ 136 | "./jest.globals.js" 137 | ], 138 | 139 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 140 | // setupFilesAfterEnv: [], 141 | 142 | // The number of seconds after which a test is considered as slow and reported as such in the results. 143 | // slowTestThreshold: 5, 144 | 145 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 146 | // snapshotSerializers: [], 147 | 148 | // The test environment that will be used for testing 149 | testEnvironment: "jsdom", 150 | 151 | // Options that will be passed to the testEnvironment 152 | // testEnvironmentOptions: {}, 153 | 154 | // Adds a location field to test results 155 | // testLocationInResults: false, 156 | 157 | // The glob patterns Jest uses to detect test files 158 | // testMatch: [ 159 | // "**/__tests__/**/*.[jt]s?(x)", 160 | // "**/?(*.)+(spec|test).[tj]s?(x)" 161 | // ], 162 | 163 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 164 | // testPathIgnorePatterns: [ 165 | // "/node_modules/" 166 | // ], 167 | 168 | // The regexp pattern or array of patterns that Jest uses to detect test files 169 | // testRegex: [], 170 | 171 | // This option allows the use of a custom results processor 172 | // testResultsProcessor: undefined, 173 | 174 | // This option allows use of a custom test runner 175 | // testRunner: "jest-circus/runner", 176 | 177 | // A map from regular expressions to paths to transformers 178 | // transform: undefined, 179 | 180 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 181 | // transformIgnorePatterns: [ 182 | // "/node_modules/", 183 | // "\\.pnp\\.[^\\/]+$" 184 | // ], 185 | 186 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 187 | // unmockedModulePathPatterns: undefined, 188 | 189 | // Indicates whether each individual test should be reported during the run 190 | // verbose: undefined, 191 | 192 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 193 | // watchPathIgnorePatterns: [], 194 | 195 | // Whether to use watchman for file crawling 196 | // watchman: true, 197 | }; 198 | -------------------------------------------------------------------------------- /jest.globals.js: -------------------------------------------------------------------------------- 1 | // Soon Node will have fetch API and this won't be needed 2 | globalThis.fetch = require('node-fetch') 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-sprint-challenge-advanced-react", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "dev": "fkill :9000 :3000 -s && concurrently \"npm:backend\" \"npm:frontend\"", 6 | "test": "cross-env NODE_ENV=testing jest", 7 | "webpack": "cross-env NODE_ENV=production webpack", 8 | "heroku-postbuild": "npm run webpack", 9 | "frontend": "webpack serve --open", 10 | "backend": "node index.js", 11 | "start": "npm run backend" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "7.18.2", 15 | "@babel/plugin-transform-react-jsx": "7.17.12", 16 | "@babel/plugin-transform-runtime": "7.18.2", 17 | "@babel/preset-env": "7.18.2", 18 | "@babel/preset-react": "7.17.12", 19 | "@testing-library/jest-dom": "5.16.4", 20 | "@testing-library/react": "13.3.0", 21 | "@types/jest": "28.1.1", 22 | "babel-loader": "8.2.5", 23 | "babel-plugin-styled-components": "2.0.7", 24 | "concurrently": "7.2.1", 25 | "cross-env": "7.0.3", 26 | "css-loader": "6.7.1", 27 | "eslint": "8.17.0", 28 | "eslint-plugin-react": "7.30.0", 29 | "fkill-cli": "7.1.0", 30 | "html-loader": "3.1.0", 31 | "html-webpack-plugin": "5.5.0", 32 | "jest": "28.1.1", 33 | "jest-environment-jsdom": "28.1.1", 34 | "msw": "0.42.0", 35 | "nodemon": "2.0.16", 36 | "string-replace-loader": "3.1.0", 37 | "style-loader": "3.3.1", 38 | "webpack": "5.73.0", 39 | "webpack-cli": "4.9.2", 40 | "webpack-dev-server": "4.9.2" 41 | }, 42 | "dependencies": { 43 | "axios": "0.27.2", 44 | "cors": "2.8.5", 45 | "express": "4.18.1", 46 | "react": "18.1.0", 47 | "react-dom": "18.1.0", 48 | "react-router-dom": "6.3.0", 49 | "styled-components": "5.3.5", 50 | "yup": "0.32.11" 51 | }, 52 | "engines": { 53 | "node": ">=18.x", 54 | "npm": ">=9.x" 55 | }, 56 | "repository": { 57 | "type": "git", 58 | "url": "git+https://github.com/bloominstituteoftechnology/web-sprint-challenge-advanced-react.git" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin') 2 | const path = require('path') 3 | 4 | const DEVELOPMENT = 'development' 5 | const ENV = process.env.NODE_ENV || DEVELOPMENT 6 | const IS_DEV = ENV === DEVELOPMENT 7 | 8 | const HTML_LOADER = 'html-loader' 9 | const STYLE_LOADER = 'style-loader' 10 | const CSS_LOADER = 'css-loader' 11 | const BABEL_LOADER = 'babel-loader' 12 | const STRING_REPLACE_LOADER = 'string-replace-loader' 13 | 14 | const SERVER_URL = /http:\/\/localhost:9000/g 15 | const FRONTEND_PORT = 3000 16 | 17 | const INDEX_HTML_PATH = './frontend/index.html' 18 | const INDEX_JS_PATH = './frontend/index.js' 19 | const DIST_FOLDER = 'dist' 20 | const BUNDLE_FILE = 'index.js' 21 | 22 | const SOURCE_MAP = IS_DEV ? 'source-map' : false 23 | 24 | const config = { 25 | entry: INDEX_JS_PATH, 26 | mode: ENV, 27 | output: { 28 | filename: BUNDLE_FILE, 29 | publicPath: '/', 30 | path: path.resolve(__dirname, DIST_FOLDER), 31 | }, 32 | devtool: SOURCE_MAP, 33 | plugins: [ 34 | new HtmlWebpackPlugin({ 35 | template: INDEX_HTML_PATH, 36 | }), 37 | ], 38 | devServer: { 39 | static: path.join(__dirname, DIST_FOLDER), 40 | historyApiFallback: true, 41 | compress: true, 42 | port: FRONTEND_PORT, 43 | client: { logging: 'none' }, 44 | }, 45 | module: { 46 | rules: [ 47 | { 48 | test: /\.html$/i, 49 | exclude: /node_modules/, 50 | use: { loader: HTML_LOADER } 51 | }, 52 | { 53 | test: /\.m?js$/, 54 | exclude: /node_modules/, 55 | use: { loader: BABEL_LOADER }, 56 | }, 57 | { 58 | test: /\.css$/i, 59 | exclude: /node_modules/, 60 | use: [ 61 | STYLE_LOADER, 62 | CSS_LOADER, 63 | ], 64 | }, 65 | ], 66 | }, 67 | } 68 | 69 | if (!IS_DEV) { 70 | config.module.rules.push({ 71 | test: /\.m?js$/, 72 | exclude: /node_modules/, 73 | use: { 74 | loader: STRING_REPLACE_LOADER, 75 | options: { 76 | search: SERVER_URL, 77 | replace: '', 78 | }, 79 | }, 80 | }) 81 | } 82 | 83 | module.exports = config 84 | --------------------------------------------------------------------------------