├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── backend ├── data │ ├── people.js │ └── planets.js ├── helpers.js ├── mock-server.js ├── routers │ └── starwars.js └── server.js ├── frontend ├── .eslintrc.json ├── components │ ├── App.js │ └── Character.js ├── favicon.ico ├── fonts │ └── TitilliumWeb-SemiBold.ttf ├── images │ └── background.jpg ├── index.html ├── index.js └── styles │ ├── reset.css │ └── styles.css ├── jest.config.js ├── jest.globals.js ├── mvp.test.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 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/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 | .DS_Store 133 | .vscode 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 BloomTech Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sprint 6 Challenge 2 | 3 | ## Introduction 4 | 5 | Welcome to Sprint Challenge 6! Today, you'll practice using React to manipulate the DOM by fetching data from the network and building a list of Star Wars characters. 6 | 7 | This experience will give you a taste of what a take-home assignment might be like, during a hiring process for a React position. 8 | 9 | Here's an overview of the tasks you need to complete: 10 | 11 | 1. **Obtain** JSON data from a web service. 12 | 1. **Combine** data obtained from different sources into a single data structure. 13 | 1. **Render** repeatable components to the DOM using the combined data and React. 14 | 15 | To succeed in this challenge, you'll need the following technical skills: 16 | 17 | 1. **Promises** and the ability to deal with asynchronous code. 18 | 1. **Making HTTP requests** with Axios or fetch. 19 | 1. **Using React's state and effect** hooks. 20 | 1. **Creating React components** and rendering them inside other components. 21 | 1. **Looping over data** passing props into React components. 22 | 1. **Adding simple interactivity** to components using event handlers. 23 | 24 | Additionally, the following soft skills will greatly impact your performance: 25 | 26 | 1. Attention to detail. Make sure there isn't a single character out of place! 27 | 1. Perseverance. Keep trying until you figure it out! 28 | 1. Patience. Make sure to read the entire README for important information. 29 | 30 | ## Instructions 31 | 32 | You are in the middle of a hiring process for a local startup that's looking for a React developer, and you have been assigned a challenge to complete. The task involves demonstrating proficiency with basic React moves like fetching data, building interactive components and using hooks. 33 | 34 | Specifically, you need to complete a website that displays a list of Star Wars characters along with their basic information such as ID, name, date of birth, and home world. Users of the app should be able to click on a character to expand some information about the character's home world. You can refer to the [full mockup](https://bloominstituteoftechnology.github.io/W_U2_S6_sprint_challenge) to have an idea of the design. 35 | 36 | To help you complete the task, several members of your team will provide you with instructions and advice. 37 | 38 | ### 💾 DevOps Engineer 39 | 40 | **Below, your future DevOps expert will help you set up your local environment and launch the project.** 41 | 42 |
43 | Click to read 44 | 45 | --- 46 | 47 | This is a **full-stack web application** that comprises both back-end and front-end components. If deployed to production, the back-end part would run in the cloud (think Amazon Web Services or Azure), while the front-end -a React app- would execute inside the user's web browser (like Chrome for Android, or Firefox for desktop). 48 | 49 | As a front-end engineer, your focus is mainly on the files that load **on the user's device**. In this particular case, these files live inside the `frontend` folder. The `backend` folder contains a web server built in Node, containing the API needed for this project. 50 | 51 | 1. You will **clone this repository** to your computer, which will allow you to run the software locally for development and testing purposes. 52 | 53 | 1. You will navigate your terminal to the project folder **and execute `npm install`**. This will install the libraries declared inside `package.json`. Some of these packages are needed for the back-end to do its job of serving JSON data. 54 | 55 | 1. After successful installation, **execute `npm run dev` in a terminal, and `npm test` in a different terminal**. On successful start, you will load the app in Chrome **at `http://localhost:3003`**. 56 | 57 | 1. If you haven't already, install the [Eslint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) for VSCode. It will highlight syntax errors and problems right inside your editor, which saves tons of time. 58 | 59 | My job assisting you with local setup of the app is done! You will speak to our designer next. 60 | 61 | --- 62 | 63 |
64 | 65 | ### 🎨 Product Designer 66 | 67 | **Below, you will find information on how to approach the task, from your future Product Designer.** 68 | 69 |
70 | Click to read 71 | 72 | --- 73 | 74 | Collaboration between a designer and a web developer can be very powerful. Designers excel at creating amazing user experiences and have a keen eye for beauty and usability, while developers are experts in the underlying technology of the product. 75 | 76 | Your job as a web developer is to implement the design with as much fidelity as possible. While a developer might think they have a better way to arrange elements on the screen, the mocks and designs are the result of research and hard work. It's important to treat them with the respect they deserve. 77 | 78 | It's crucial to use the readable texts designed for the user interface **verbatim**. If a design reads "Loading Characters...", then "Loading _the_ Characters..." is incorrect. Attention to detail is critical! 79 | 80 | There are other constraints and requirements that must be followed, such as sticking to certain class names or keeping the structure of the HTML a certain way. 81 | 82 | Fortunately, you have [a very detailed mock](https://bloominstituteoftechnology.github.io/W_U2_S6_sprint_challenge/) that you can load in your browser and inspect closely, which will make your job much easier. 83 | 84 | --- 85 | 86 |
87 | 88 | ### 🥷 Lead Developer 89 | 90 | **Below, your future Team Lead will discuss strategy and tactics for dealing with this ticket.** 91 | 92 |
93 | Click to read 94 | 95 | --- 96 | 97 | Hey! Let's make sure you're up to speed with your **action items so far**. 98 | 99 | - [x] The app is installed on your machine, with both `dev` and `test` scripts running in terminals. 100 | - [x] You studied the [mock](https://bloominstituteoftechnology.github.io/W_U2_S6_sprint_challenge/) in the Elements tab of Dev Tools. 101 | - [x] You saw how the "planet" paragraph is mounted and unmounted from the DOM as the user clicks on a character. 102 | 103 | Awesome! Our back-end engineer says that the JSON data needed to build the Star Wars Character Cards comes from two endpoints: 104 | 105 | - Endpoint A [GET] 106 | - Endpoint B [GET] 107 | 108 | ❗ You should stop now, and **try out both endpoints using Postman**, to see what they return. 109 | 110 | Inside `frontend/components/App.js`, and **on first render only**, fetch the data from both endpoints above. 111 | 112 | Here's the tricky thing: each character fetched using Endpoint A has a "homeworld" property, but the value of this property is not the name of the planet but its ID instead. This means you must use the data obtained from Endpoint B to obtain the missing piece of information about each character: the name of their home world. 113 | 114 | For fetching, you can optionally use `Promise.all` to handle the requests. We do not need the data from request A in order to _start_ request B, so the requests can happen concurrently instead of back-to-back. **This will make the app feel faster** to the user! 115 | 116 | Once you have the responses from Endpoints A and B stored inside variables, check that they match what you saw in Postman, and then **use your JavaScript skills to combine the two lists into a single data structure** that is comfortable to work with. There may be some array methods that can help with this...Ideally, it would look something like this: 117 | 118 | ```js 119 | [ 120 | { 121 | id: 18, 122 | name: "Luke Skywalker", 123 | // etc 124 | homeworld: { 125 | id: 31, 126 | name: "Tatooine", 127 | // etc 128 | } 129 | }, 130 | // other characters 131 | ] 132 | ``` 133 | 134 | Once you have the data in the right shape stored in App state, you can **start working on your `/frontend/components/Character.js` component** that takes in the information about a single character via props. You will use this component inside App.js, looping over the data held in App state and rendering a Character at each iteration of the loop. 135 | 136 | Make sure that each character that renders to the DOM has the **exact same class names and text contents** as those in the design! Also, render the characters **in the same order** as they arrive from Endpoint A. 137 | 138 | Note how for each card, the planet information is not available on page load. A paragraph containing the name of a given character's home world appears in the DOM when a user clicks on the character's card, as you can see in the [mock](https://bloominstituteoftechnology.github.io/W_U2_S6_sprint_challenge). Clicking again on the character unmounts the paragraph completely, removing the planet information from the DOM. Whether the home world name shows or not is private component state of the Character component. 139 | 140 | Reach out if you get too stuck, and have fun! 141 | 142 | --- 143 | 144 |
145 | 146 | ## FAQ 147 | 148 |
149 | How do I submit this task? 150 | 151 | You submit via Codegrade. Check the assignment page on your learning platform. 152 | 153 |
154 | 155 |
156 | I am getting errors when I run npm install or npm run dev. What is going on? 157 | 158 | This project requires Node correctly installed on your computer in order to work. Your learning materials should have covered installation of Node. Sometimes Node can be installed but mis-configured. You can try executing `npm run install:violently` (check out `package.json` to see what this does), but if Node errors are recurrent, it indicates something is wrong with your machine or configuration, in which case you should request assistance from Staff. 159 | 160 |
161 | 162 |
163 | Do I need to install any libraries? 164 | 165 | No. Everything you need should be installed already, including Axios. 166 | 167 |
168 | 169 |
170 | Am I allowed to edit the styling of the site? 171 | 172 | You are welcome to add your personal touch to the site using Styled Components. There is just one rule: all tests must pass! Our recommendation is to wait until MVP is achieved before editing the styles. 173 | 174 |
175 | 176 |
177 | My app does not work! How do I debug it? 178 | 179 | Save your changes, and reload the site in Chrome. If you have a syntax problem in your code, the app will print error messages in the Console. Focus on the first message. Place console logs right before the crash site (errors usually inform of the line number where the problem is originating) and see if your variables contain the data you think they do. If there are no errors but the page is not doing what it's supposed to, the debugging technique is similar: put console logs to ensure that the code you are working on is actually executing, and to check that all variables in the area hold the correct data. 180 | 181 |
182 | 183 |
184 | How do I run tests against my code? 185 | 186 | Execute `npm test` in your terminal. These are the same tests that execute inside Codegrade. Although this never crossed your mind, tampering with the test file won't change your score, because Codegrade uses a pristine copy of the original test file, `mvp.test.js`. If a particular test is giving you grief, don't jump straight to the code to try and fix it. Go to Chrome first, and make sure you can replicate the problem there. A problem we can reliably replicate is a problem mostly fixed. 187 | 188 |
189 | 190 |
191 | I believe my code is correct and the test is wrong. What do I do? 192 | 193 | On occasion the test runner will get stuck. Use CTRL-C to kill the tests, and then `npm test` to launch them again. Try to reproduce the problem the test is complaining about by interacting with the site in Chrome, and do not code "to make the test happy". Code so that **your app does exactly what the mock does**. The tests are there for confirmation. Although it's possible that a particular test be flawed, it's more likely that the bug is in your own code. If the problem persists, please request assistance from Staff. 194 | 195 |
196 | 197 |
198 | The output of the test script is just too overwhelming! What can I do? 199 | 200 | If you need to disable all tests except the one you are focusing on, edit the `mvp.test.js` file and, as an example, change `test('👉 focus on this', () => { etc })` to be `test.only('👉 focus on this', () => { etc })`. (Note the "only".) This won't affect Codegrade, because Codegrade runs its own version of the tests. Keep in mind though, if there is a syntax problem with your code that is causing an error to be thrown, all tests will fail. 201 | 202 |
203 | 204 |
205 | Why can't endpoints provide the data in the correct shape from the get-go? 206 | 207 | As web developers, we often don't have control over our sources of data, and it's common to have to combine JSON from various sources into a data structure that works for the front-end. Even if the endpoints were under our control, and the back-end team were willing to build a new endpoint or improve existing ones, bug fixes and features sometimes can't wait that long. 208 | 209 |
210 | 211 |
212 | I messed up and want to start over! How do I do that? 213 | 214 | **Do NOT delete your repository from GitHub!** Instead, commit _frequently_ as you work. Make a commit whenever you achieve _anything_ and the app isn't crashing in Chrome. This in practice creates restore points you can use should you wreak havoc with your app. If you find yourself in a mess, use `git reset --hard` to simply discard all changes to your code since your last commit. If you are dead-set on restarting the challenge from scratch, you can do this with Git as well, but it is advised that you request assistance from Staff. 215 | 216 |
217 | 218 |
219 | Why are there so many files in this project? 220 | 221 | Although a small, "old-fashioned" website might be made of just HTML, CSS and JS files, these days we mostly manage projects with Node and its package manager, NPM. Node apps typically have a `package.json` file and several other configuration files placed at the root of the project. This project also includes automated tests and a web server, which adds a little bit of extra complexity and files. 222 | 223 |
224 | 225 |
226 | Is this how React projects are normally organized? 227 | 228 | React projects can be organized in a million ways, there aren't many standards. Some developers like it like this, while others prefer to use opinionated frameworks, which do a lot of magic but prescribe that folders and files be structured and named just so. 229 | 230 |
231 | 232 |
233 | What are the package.json and package-lock.json files? 234 | 235 | The `package.json` file contains meta-information about the project like its version number, scripts that the developer can execute, and a list of the dependencies that are downloaded when you execute `npm install`. There can be some wiggle room to allow newer versions of the dependencies to be installed, so the `package-lock.json` file, when present, makes sure the exact same versions of everything are used every time the project is installed from scratch. 236 | 237 |
238 | 239 |
240 | What is the .eslintrc.js file? 241 | 242 | This file works in combination with the Eslint extension for VSCode to highlight syntax errors and problems in your code. By editing this file you can customize your linting rules. 243 | 244 |
245 | 246 |
247 | What is Jest? 248 | 249 | Jest is a framework that allows you to write tests and execute them, to alert you very quickly of problems with the code. Jest can do in seconds what an entire Quality Assurance team would take hours or even days. In the context of the Sprint Challenge, Jest is used to check your code against specification and give you a grade (% of tests passing). 250 | 251 |
252 | 253 | **Project created with [@bloomtools/react@0.1.7](https://github.com/bloominstituteoftechnology/npm-tools-react) and Node v18.16.0 on Thu, July 06, 2023 at 03:27 PM** 254 | -------------------------------------------------------------------------------- /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: '110' } }] 25 | ] 26 | }, 27 | production: { 28 | plugins: [ 29 | [PLUGIN_STYLED_COMPONENTS], 30 | ], 31 | presets: [ 32 | [PRESET_REACT], 33 | [PRESET_ENV, { targets: { chrome: '110' } }] 34 | ] 35 | }, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/data/people.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | "id": 18, 4 | "name": "Luke Skywalker", 5 | "birth_year": "19BBY", 6 | "created": "2014-12-09T13:50:51.644000Z", 7 | "edited": "2014-12-20T21:17:56.891000Z", 8 | "eye_color": "blue", 9 | "gender": "male", 10 | "hair_color": "blond", 11 | "height": "172", 12 | "mass": "77", 13 | "skin_color": "fair", 14 | "homeworld": 31 15 | }, 16 | { 17 | "id": 52, 18 | "name": "C-3PO", 19 | "birth_year": "112BBY", 20 | "created": "2014-12-10T15:10:51.357000Z", 21 | "edited": "2014-12-20T21:17:50.309000Z", 22 | "eye_color": "yellow", 23 | "gender": "n/a", 24 | "hair_color": "n/a", 25 | "height": "167", 26 | "mass": "75", 27 | "skin_color": "gold", 28 | "homeworld": 31 29 | }, 30 | { 31 | "id": 71, 32 | "name": "R2-D2", 33 | "birth_year": "33BBY", 34 | "created": "2014-12-10T15:11:50.376000Z", 35 | "edited": "2014-12-20T21:17:50.311000Z", 36 | "eye_color": "red", 37 | "gender": "n/a", 38 | "hair_color": "n/a", 39 | "height": "96", 40 | "mass": "32", 41 | "skin_color": "white, blue", 42 | "homeworld": 28 43 | }, 44 | { 45 | "id": 7, 46 | "name": "Darth Vader", 47 | "birth_year": "41.9BBY", 48 | "created": "2014-12-10T15:18:20.704000Z", 49 | "edited": "2014-12-20T21:17:50.313000Z", 50 | "eye_color": "yellow", 51 | "gender": "male", 52 | "hair_color": "none", 53 | "height": "202", 54 | "mass": "136", 55 | "skin_color": "white", 56 | "homeworld": 31 57 | }, 58 | { 59 | "id": 65, 60 | "name": "Leia Organa", 61 | "birth_year": "19BBY", 62 | "created": "2014-12-10T15:20:09.791000Z", 63 | "edited": "2014-12-20T21:17:50.315000Z", 64 | "eye_color": "brown", 65 | "gender": "female", 66 | "hair_color": "brown", 67 | "height": "150", 68 | "mass": "49", 69 | "skin_color": "light", 70 | "homeworld": 12 71 | }, 72 | { 73 | "id": 88, 74 | "name": "Owen Lars", 75 | "birth_year": "52BBY", 76 | "created": "2014-12-10T15:52:14.024000Z", 77 | "edited": "2014-12-20T21:17:50.317000Z", 78 | "eye_color": "blue", 79 | "gender": "male", 80 | "hair_color": "brown, grey", 81 | "height": "178", 82 | "mass": "120", 83 | "skin_color": "light", 84 | "homeworld": 31 85 | }, 86 | { 87 | "id": 82, 88 | "name": "Beru Whitesun Lars", 89 | "birth_year": "47BBY", 90 | "created": "2014-12-10T15:53:41.121000Z", 91 | "edited": "2014-12-20T21:17:50.319000Z", 92 | "eye_color": "blue", 93 | "gender": "female", 94 | "hair_color": "brown", 95 | "height": "165", 96 | "mass": "75", 97 | "skin_color": "light", 98 | "homeworld": 31 99 | }, 100 | { 101 | "id": 47, 102 | "name": "R5-D4", 103 | "birth_year": "unknown", 104 | "created": "2014-12-10T15:57:50.959000Z", 105 | "edited": "2014-12-20T21:17:50.321000Z", 106 | "eye_color": "red", 107 | "gender": "n/a", 108 | "hair_color": "n/a", 109 | "height": "97", 110 | "mass": "32", 111 | "skin_color": "white, red", 112 | "homeworld": 93 113 | }, 114 | { 115 | "id": 39, 116 | "name": "Yoda", 117 | "birth_year": "896BBY", 118 | "created": "2014-12-10T15:59:50.509000Z", 119 | "edited": "2014-12-20T21:17:50.323000Z", 120 | "eye_color": "brown", 121 | "gender": "male", 122 | "hair_color": "white", 123 | "height": "66", 124 | "skin_color": "green", 125 | "homeworld": 5 126 | } 127 | ] 128 | -------------------------------------------------------------------------------- /backend/data/planets.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | "id": 31, 4 | "name": "Tatooine", 5 | "climate": "arid", 6 | "created": "2014-12-09T13:50:49.641000Z", 7 | "diameter": "10465", 8 | "edited": "2014-12-20T20:58:18.411000Z", 9 | "gravity": "1 standard", 10 | "orbital_period": "304", 11 | "population": "200000", 12 | "rotation_period": "23", 13 | "surface_water": "1", 14 | "terrain": "desert" 15 | }, 16 | { 17 | "id": 12, 18 | "name": "Alderaan", 19 | "climate": "temperate", 20 | "created": "2014-12-10T11:35:48.479000Z", 21 | "diameter": "12500", 22 | "edited": "2014-12-20T20:58:18.420000Z", 23 | "gravity": "1 standard", 24 | "orbital_period": "364", 25 | "population": "2000000000", 26 | "rotation_period": "24", 27 | "surface_water": "40", 28 | "terrain": "grasslands, mountains" 29 | }, 30 | { 31 | "id": 38, 32 | "name": "Yavin IV", 33 | "climate": "temperate, tropical", 34 | "created": "2014-12-10T11:37:19.144000Z", 35 | "diameter": "10200", 36 | "edited": "2014-12-20T20:58:18.421000Z", 37 | "gravity": "1 standard", 38 | "orbital_period": "4818", 39 | "population": "1000", 40 | "rotation_period": "24", 41 | "surface_water": "8", 42 | "terrain": "jungle, rainforests" 43 | }, 44 | { 45 | "id": 19, 46 | "name": "Hoth", 47 | "climate": "frozen", 48 | "created": "2014-12-10T11:39:13.934000Z", 49 | "diameter": "7200", 50 | "edited": "2014-12-20T20:58:18.423000Z", 51 | "gravity": "1.1 standard", 52 | "orbital_period": "549", 53 | "population": "unknown", 54 | "rotation_period": "23", 55 | "surface_water": "100", 56 | "terrain": "tundra, ice caves, mountain ranges" 57 | }, 58 | { 59 | "id": 5, 60 | "name": "Dagobah", 61 | "climate": "murky", 62 | "created": "2014-12-10T11:42:22.590000Z", 63 | "diameter": "8900", 64 | "edited": "2014-12-20T20:58:18.425000Z", 65 | "gravity": "N/A", 66 | "orbital_period": "341", 67 | "population": "unknown", 68 | "rotation_period": "23", 69 | "surface_water": "8", 70 | "terrain": "swamp, jungles" 71 | }, 72 | { 73 | "id": 84, 74 | "name": "Bespin", 75 | "climate": "temperate", 76 | "created": "2014-12-10T11:43:55.240000Z", 77 | "diameter": "118000", 78 | "edited": "2014-12-20T20:58:18.427000Z", 79 | "gravity": "1.5 (surface), 1 standard (Cloud City)", 80 | "orbital_period": "5110", 81 | "population": "6000000", 82 | "rotation_period": "12", 83 | "surface_water": "0", 84 | "terrain": "gas giant" 85 | }, 86 | { 87 | "id": 54, 88 | "name": "Endor", 89 | "climate": "temperate", 90 | "created": "2014-12-10T11:50:29.349000Z", 91 | "diameter": "4900", 92 | "edited": "2014-12-20T20:58:18.429000Z", 93 | "gravity": "0.85 standard", 94 | "orbital_period": "402", 95 | "population": "30000000", 96 | "rotation_period": "18", 97 | "surface_water": "8", 98 | "terrain": "forests, mountains, lakes" 99 | }, 100 | { 101 | "id": 28, 102 | "name": "Naboo", 103 | "climate": "temperate", 104 | "created": "2014-12-10T11:52:31.066000Z", 105 | "diameter": "12120", 106 | "edited": "2014-12-20T20:58:18.430000Z", 107 | "gravity": "1 standard", 108 | "orbital_period": "312", 109 | "population": "4500000000", 110 | "rotation_period": "26", 111 | "surface_water": "12", 112 | "terrain": "grassy hills, swamps, forests, mountains" 113 | }, 114 | { 115 | "id": 93, 116 | "name": "Coruscant", 117 | "climate": "temperate", 118 | "created": "2014-12-10T11:54:13.921000Z", 119 | "diameter": "12240", 120 | "edited": "2014-12-20T20:58:18.432000Z", 121 | "gravity": "1 standard", 122 | "orbital_period": "368", 123 | "population": "1000000000000", 124 | "rotation_period": "26", 125 | "surface_water": "12", 126 | "terrain": "grassy hills, swamps, forests, mountains" 127 | } 128 | ] 129 | -------------------------------------------------------------------------------- /backend/helpers.js: -------------------------------------------------------------------------------- 1 | const people = require('./data/people') 2 | const planets = require('./data/planets') 3 | 4 | const getPeople = () => people 5 | const getPlanets = () => planets 6 | 7 | module.exports = { 8 | getPeople, 9 | getPlanets, 10 | } 11 | -------------------------------------------------------------------------------- /backend/mock-server.js: -------------------------------------------------------------------------------- 1 | const { setupServer } = require('msw/node') 2 | const { rest } = require('msw') 3 | const StarWars = require('./helpers') 4 | 5 | function getPeople(req, res, ctx) { 6 | return res( 7 | ctx.json(StarWars.getPeople()), 8 | ) 9 | } 10 | 11 | function getPlanets(req, res, ctx) { 12 | return res( 13 | ctx.json(StarWars.getPlanets()), 14 | ) 15 | } 16 | 17 | const handlers = [ 18 | rest.get('http://localhost:9009/api/people', getPeople), 19 | rest.get('http://localhost:9009/api/planets', getPlanets), 20 | ] 21 | 22 | module.exports = setupServer(...handlers) 23 | -------------------------------------------------------------------------------- /backend/routers/starwars.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const StarWars = require('../helpers') 3 | 4 | const router = express.Router() 5 | 6 | router.get('/people', (req, res) => { 7 | res.json(StarWars.getPeople()) 8 | }) 9 | 10 | router.get('/planets', (req, res) => { 11 | res.json(StarWars.getPlanets()) 12 | }) 13 | 14 | module.exports = router 15 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const cors = require('cors') 3 | const path = require('path') 4 | const starWarsRouter = require('./routers/starwars') 5 | 6 | const PORT = process.env.PORT || 9009 7 | 8 | const server = express() 9 | 10 | server.use(express.json()) 11 | 12 | server.use(express.static(path.join(__dirname, '../dist'))) 13 | 14 | server.use(cors()) 15 | 16 | server.use('/api', starWarsRouter) 17 | 18 | server.get('*', (req, res) => { 19 | res.sendFile(path.join(__dirname, '../dist/index.html')) 20 | }) 21 | 22 | server.use((req, res) => { 23 | res.status(404).json({ 24 | message: `Endpoint [${req.method}] ${req.path} does not exist`, 25 | }) 26 | }) 27 | 28 | server.listen(PORT, () => { 29 | console.log(`listening on ${PORT}`) 30 | }) 31 | -------------------------------------------------------------------------------- /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.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import axios from 'axios' 3 | import Character from './Character' 4 | 5 | const urlPlanets = 'http://localhost:9009/api/planets' 6 | const urlPeople = 'http://localhost:9009/api/people' 7 | 8 | function App() { 9 | // ❗ Create state to hold the data from the API 10 | // ❗ Create effects to fetch the data and put it in state 11 | return ( 12 |
13 |

Star Wars Characters

14 |

See the README of the project for instructions on completing this challenge

15 | {/* ❗ Map over the data in state, rendering a Character at each iteration */} 16 |
17 | ) 18 | } 19 | 20 | export default App 21 | 22 | // ❗ DO NOT CHANGE THE CODE BELOW 23 | if (typeof module !== 'undefined' && module.exports) module.exports = App 24 | -------------------------------------------------------------------------------- /frontend/components/Character.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function Character() { // ❗ Add the props 4 | // ❗ Create a state to hold whether the homeworld is rendering or not 5 | // ❗ Create a "toggle" click handler to show or remove the homeworld 6 | return ( 7 |
8 | {/* Use the same markup with the same attributes as in the mock */} 9 |
10 | ) 11 | } 12 | 13 | export default Character 14 | -------------------------------------------------------------------------------- /frontend/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloominstituteoftechnology/web-sprint-challenge-intro-to-react/f7593d1824029bc93c372266604e229517d623be/frontend/favicon.ico -------------------------------------------------------------------------------- /frontend/fonts/TitilliumWeb-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloominstituteoftechnology/web-sprint-challenge-intro-to-react/f7593d1824029bc93c372266604e229517d623be/frontend/fonts/TitilliumWeb-SemiBold.ttf -------------------------------------------------------------------------------- /frontend/images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloominstituteoftechnology/web-sprint-challenge-intro-to-react/f7593d1824029bc93c372266604e229517d623be/frontend/images/background.jpg -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Sprint 6 Challenge 9 | 10 | 11 | 12 |
13 |

Sprint 6 Challenge

14 |
15 | 16 |
17 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /frontend/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './components/App' 4 | import './styles/reset.css' 5 | import './styles/styles.css' 6 | 7 | const domNode = document.getElementById('root') 8 | const root = createRoot(domNode) 9 | 10 | root.render( 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /frontend/styles/reset.css: -------------------------------------------------------------------------------- 1 | /* RESET BY https://www.joshwcomeau.com/css/custom-css-reset */ 2 | *, 3 | *::before, 4 | *::after { 5 | box-sizing: border-box; 6 | } 7 | 8 | * { 9 | margin: 0; 10 | } 11 | 12 | html, 13 | body { 14 | height: 100%; 15 | } 16 | 17 | body { 18 | line-height: 1.5; 19 | -webkit-font-smoothing: antialiased; 20 | } 21 | 22 | img, 23 | picture, 24 | video, 25 | canvas, 26 | svg { 27 | display: block; 28 | max-width: 100%; 29 | } 30 | 31 | input, 32 | button, 33 | textarea, 34 | select { 35 | font: inherit; 36 | } 37 | 38 | p, 39 | h1, 40 | h2, 41 | h3, 42 | h4, 43 | h5, 44 | h6 { 45 | overflow-wrap: break-word; 46 | } 47 | 48 | #root, 49 | #__next { 50 | isolation: isolate; 51 | } 52 | -------------------------------------------------------------------------------- /frontend/styles/styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Titillium Web'; 3 | src: url('../fonts/TitilliumWeb-SemiBold.ttf'); 4 | } 5 | 6 | body { 7 | font-family: 'Titillium Web', sans-serif; 8 | border: 1rem solid #ff4b00; 9 | background-image: url('../images/background.jpg'); 10 | background-size: cover; 11 | } 12 | 13 | #root, 14 | body, 15 | footer { 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | } 20 | 21 | h1, 22 | h2, 23 | h3 { 24 | color: rgb(170, 0, 0); 25 | } 26 | 27 | #root { 28 | width: 100%; 29 | overflow-y: auto; 30 | border-top: 1px solid grey; 31 | border-bottom: 1px solid grey; 32 | padding-top: 2rem; 33 | padding-bottom: 2rem; 34 | background-color: white; 35 | opacity: 0.8; 36 | } 37 | 38 | .character-card { 39 | transition: all 0.3s ease-in-out; 40 | margin: 1.2rem; 41 | padding: 1.2rem; 42 | border: 1px solid lightgray; 43 | background-color: rgb(246, 246, 246); 44 | border-radius: 15px; 45 | } 46 | 47 | .character-card:hover { 48 | transition: all 0.3s ease-in-out; 49 | background-color: white; 50 | box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px; 51 | } 52 | 53 | .character-card h3:hover { 54 | cursor: pointer; 55 | text-decoration: underline; 56 | } 57 | 58 | .character-card p { 59 | color: gray; 60 | } 61 | 62 | .character-planet { 63 | color: black; 64 | } 65 | 66 | header, 67 | footer { 68 | padding: 1rem; 69 | color: white; 70 | } 71 | -------------------------------------------------------------------------------- /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/96/mtz831_d14j85kmn9jcn1mc40000gn/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 | // This makes fetch and axios work in the tests 2 | const axios = require('axios') 3 | const nodeFetch = require('node-fetch') 4 | 5 | globalThis.axios = axios 6 | globalThis.fetch = nodeFetch 7 | globalThis.Request = nodeFetch.Request 8 | globalThis.Response = nodeFetch.Response 9 | -------------------------------------------------------------------------------- /mvp.test.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const { render, fireEvent, screen, within } = require('@testing-library/react') 3 | require('@testing-library/jest-dom/extend-expect') 4 | const server = require('./backend/mock-server') 5 | const App = require('./frontend/components/App') 6 | const people = require('./backend/data/people') 7 | const planets = require('./backend/data/planets') 8 | 9 | jest.setTimeout(750) 10 | const waitForOptions = { timeout: 100 } 11 | const queryOptions = { exact: false } 12 | 13 | const renderApp = ui => { 14 | window.localStorage.clear() 15 | window.history.pushState({}, 'Test page', '/') 16 | return render(ui) 17 | } 18 | 19 | beforeAll(() => { server.listen() }) 20 | afterAll(() => { server.close() }) 21 | beforeEach(() => { renderApp() }) 22 | afterEach(() => { server.resetHandlers() }) 23 | 24 | describe('Sprint 6 Challenge', () => { 25 | test('[1] Luke Skywalker\'s name is not in the DOM after first render', async () => { 26 | expect(screen.queryByText('Luke Skywalker', queryOptions, waitForOptions)).not.toBeInTheDocument() 27 | await screen.findByText('Luke Skywalker', queryOptions, waitForOptions) 28 | }) 29 | test('[2] Luke Skywalker\'s name renders to the DOM eventually', async () => { 30 | expect(await screen.findByText('Luke Skywalker', queryOptions, waitForOptions)).toBeInTheDocument() 31 | }) 32 | test('[3] All character cards are rendered with a class of "character-card"', async () => { 33 | await screen.findByText('Luke Skywalker', queryOptions, waitForOptions) 34 | const characters = document.querySelectorAll('.character-card') 35 | expect(characters).toHaveLength(9) 36 | }) 37 | test('[4] Character cards have the correct character name', async () => { 38 | await screen.findByText('Luke Skywalker', queryOptions, waitForOptions) 39 | const characters = document.querySelectorAll('.character-card') 40 | characters.forEach((charElement, idx) => { 41 | within(charElement).getByText(people[idx].name, queryOptions) 42 | }) 43 | }) 44 | test('[5] Character names are rendered with a class of "character-name"', async () => { 45 | await screen.findByText('Luke Skywalker', queryOptions, waitForOptions) 46 | const nameHeadings = document.querySelectorAll('.character-name') 47 | expect(nameHeadings).toHaveLength(9) 48 | }) 49 | test('[6] Character cards do not render the planets by default', async () => { 50 | await screen.findByText('Luke Skywalker', queryOptions, waitForOptions) 51 | const characters = document.querySelectorAll('.character-card') 52 | characters.forEach((charElement, idx) => { 53 | const pl = planets.find(p => p.id == people[idx].homeworld).name 54 | expect(within(charElement).queryByText(pl, queryOptions)).not.toBeInTheDocument() 55 | }) 56 | }) 57 | test('[7] Character cards will render their planet after clicking on the card', async () => { 58 | await screen.findByText('Luke Skywalker', queryOptions, waitForOptions) 59 | const characters = document.querySelectorAll('.character-card') 60 | const luke = characters[0] 61 | const leia = characters[4] 62 | fireEvent.click(luke) 63 | fireEvent.click(leia) 64 | characters.forEach((charElement, idx) => { 65 | const pl = planets.find(p => p.id == people[idx].homeworld).name 66 | if (charElement === luke || charElement === leia) { 67 | within(charElement).getByText(pl, queryOptions) 68 | } else { 69 | expect(within(charElement).queryByText(pl, queryOptions)).not.toBeInTheDocument() 70 | } 71 | }) 72 | }) 73 | test('[8] Character cards will hide their planet after clicking again on the card', async () => { 74 | await screen.findByText('Luke Skywalker', queryOptions, waitForOptions) 75 | const characters = document.querySelectorAll('.character-card') 76 | const luke = characters[0] 77 | const leia = characters[4] 78 | fireEvent.click(luke) 79 | fireEvent.click(leia) 80 | characters.forEach((charElement) => { 81 | fireEvent.click(charElement) 82 | }) 83 | characters.forEach((charElement, idx) => { 84 | const pl = planets.find(p => p.id == people[idx].homeworld).name 85 | if (charElement === luke || charElement === leia) { 86 | expect(within(charElement).queryByText(pl, queryOptions)).not.toBeInTheDocument() 87 | } else { 88 | within(charElement).getByText(pl, queryOptions) 89 | } 90 | }) 91 | }) 92 | test('[9] Character planets are rendered with a class of "character-planet"', async () => { 93 | await screen.findByText('Luke Skywalker', queryOptions, waitForOptions) 94 | const characters = document.querySelectorAll('.character-card') 95 | characters.forEach((charElement) => { 96 | fireEvent.click(charElement) 97 | }) 98 | const planets = document.querySelectorAll('.character-planet') 99 | expect(planets).toHaveLength(9) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-project", 3 | "version": "0.1.2", 4 | "scripts": { 5 | "dev": "fkill :9009 :3003 -f -s && concurrently \"npm:backend\" \"npm:frontend\"", 6 | "test": "cross-env NODE_ENV=testing jest --watchAll", 7 | "build": "cross-env NODE_ENV=production webpack", 8 | "frontend": "webpack serve --open", 9 | "backend": "node backend/server.js", 10 | "start": "npm run backend", 11 | "install:violently": "rm -rf node_modules package-lock.json && npm cache clean --force && npm i" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "^7.22.10", 15 | "@babel/plugin-transform-react-jsx": "^7.22.5", 16 | "@babel/plugin-transform-runtime": "^7.22.10", 17 | "@babel/preset-env": "^7.22.10", 18 | "@babel/preset-react": "^7.22.5", 19 | "@testing-library/jest-dom": "^5.17.0", 20 | "@testing-library/react": "^14.0.0", 21 | "@types/jest": "^29.5.3", 22 | "babel-loader": "^9.1.3", 23 | "babel-plugin-styled-components": "^2.1.4", 24 | "concurrently": "^8.2.0", 25 | "cross-env": "^7.0.3", 26 | "css-loader": "^6.8.1", 27 | "eslint": "^8.46.0", 28 | "eslint-plugin-react": "^7.33.1", 29 | "file-loader": "^6.2.0", 30 | "fkill-cli": "^7.1.0", 31 | "html-loader": "^4.2.0", 32 | "html-webpack-plugin": "^5.5.3", 33 | "jest": "^29.6.2", 34 | "jest-environment-jsdom": "^29.6.2", 35 | "msw": "^1.2.3", 36 | "nodemon": "^3.0.1", 37 | "string-replace-loader": "^3.1.0", 38 | "style-loader": "^3.3.3", 39 | "webpack": "^5.88.2", 40 | "webpack-cli": "^5.1.4", 41 | "webpack-dev-server": "^4.15.1" 42 | }, 43 | "dependencies": { 44 | "axios": "^1.4.0", 45 | "cors": "^2.8.5", 46 | "express": "^4.18.2", 47 | "react": "^18.2.0", 48 | "react-dom": "^18.2.0", 49 | "react-router-dom": "^6.15.0", 50 | "styled-components": "^6.0.7", 51 | "yup": "^1.2.0" 52 | }, 53 | "engines": { 54 | "node": ">=16.x", 55 | "npm": ">=8.x" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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 | const FILE_LOADER = 'file-loader' 14 | 15 | const SERVER_URL = /http:\/\/localhost:9009/g 16 | const FRONTEND_PORT = 3003 17 | 18 | const INDEX_HTML_PATH = './frontend/index.html' 19 | const INDEX_JS_PATH = './frontend/index.js' 20 | const DIST_FOLDER = 'dist' 21 | const BUNDLE_FILE = 'index.js' 22 | const AUDIO = 'audio/' 23 | 24 | const SOURCE_MAP = IS_DEV ? 'source-map' : false 25 | 26 | const config = { 27 | entry: INDEX_JS_PATH, 28 | mode: ENV, 29 | output: { 30 | filename: BUNDLE_FILE, 31 | publicPath: '/', 32 | path: path.resolve(__dirname, DIST_FOLDER), 33 | }, 34 | devtool: SOURCE_MAP, 35 | plugins: [ 36 | new HtmlWebpackPlugin({ 37 | template: INDEX_HTML_PATH, 38 | }), 39 | ], 40 | devServer: { 41 | static: path.join(__dirname, DIST_FOLDER), 42 | historyApiFallback: true, 43 | compress: true, 44 | port: FRONTEND_PORT, 45 | client: { logging: 'none' }, 46 | }, 47 | module: { 48 | rules: [ 49 | { 50 | test: /\.html$/i, 51 | exclude: /node_modules/, 52 | use: { loader: HTML_LOADER } 53 | }, 54 | { 55 | test: /\.m?js$/, 56 | exclude: /node_modules/, 57 | use: { loader: BABEL_LOADER }, 58 | }, 59 | { 60 | test: /\.css$/i, 61 | use: [ 62 | STYLE_LOADER, 63 | CSS_LOADER, 64 | ], 65 | }, 66 | { 67 | test: /\.(png|jpe?g|gif|svg)$/i, 68 | type: 'asset/resource' 69 | }, 70 | { 71 | test: /\.mp3$/, 72 | use: [ 73 | { 74 | loader: FILE_LOADER, 75 | options: { 76 | name: '[name].[ext]', 77 | outputPath: AUDIO, 78 | publicPath: AUDIO, 79 | }, 80 | }, 81 | ], 82 | }, 83 | ], 84 | }, 85 | } 86 | 87 | if (!IS_DEV) { 88 | config.module.rules.push({ 89 | test: /\.m?js$/, 90 | exclude: /node_modules/, 91 | use: { 92 | loader: STRING_REPLACE_LOADER, 93 | options: { 94 | search: SERVER_URL, 95 | replace: '', 96 | }, 97 | }, 98 | }) 99 | } 100 | 101 | module.exports = config 102 | --------------------------------------------------------------------------------