├── .DS_Store ├── .babelrc ├── .eslintrc.json ├── .github └── workflows │ └── linters.yml ├── .gitignore ├── .stylelintrc.json ├── LICENSE ├── README.md ├── murple_logo.png ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── __tests__ ├── Calculator.test.js ├── FetchQuote.test.js ├── Layout.test.js ├── Navbar.test.js ├── NumbersBtns.test.js ├── OperatorBtns.test.js ├── __snapshots__ │ ├── Calculator.test.js.snap │ ├── FetchQuote.test.js.snap │ ├── Layout.test.js.snap │ ├── Navbar.test.js.snap │ ├── NumbersBtns.test.js.snap │ ├── OperatorBtns.test.js.snap │ └── operate.test.js.snap ├── calculate.test.js └── operate.test.js ├── components ├── Calculator.js ├── FetchQuote.js ├── Layout.js ├── Navbar.js ├── NumbersBtns.js └── OperatorBtns.js ├── index.css ├── index.js ├── logic ├── calculate.js └── operate.js ├── logo.svg ├── routes ├── Calculate.js ├── Home.js ├── NotMatch.js └── Quote.js └── styles └── style.scss /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Salimer/Math-magicians/fdbe626b980c27cc3fdf57e8c5dc273816a3fb18/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react" 4 | ], 5 | "plugins": ["@babel/plugin-syntax-jsx"] 6 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "parser": "@babel/eslint-parser", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "ecmaVersion": 2018, 13 | "sourceType": "module" 14 | }, 15 | "extends": ["airbnb", "plugin:react/recommended", "plugin:react-hooks/recommended"], 16 | "plugins": ["react"], 17 | "rules": { 18 | "react/jsx-filename-extension": ["warn", { "extensions": [".js", ".jsx"] }], 19 | "react/react-in-jsx-scope": "off", 20 | "import/no-unresolved": "off", 21 | "no-shadow": "off" 22 | }, 23 | "overrides": [ 24 | { 25 | "files": ["src/**/*Slice.js"], 26 | // avoid state param assignment 27 | "rules": { "no-param-reassign": ["error", { "props": false }] } 28 | } 29 | ], 30 | "ignorePatterns": [ 31 | "dist/", 32 | "build/" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: pull_request 4 | 5 | env: 6 | FORCE_COLOR: 1 7 | 8 | jobs: 9 | eslint: 10 | name: ESLint 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: "18.x" 17 | - name: Setup ESLint 18 | run: | 19 | npm install --save-dev eslint@7.x eslint-config-airbnb@18.x eslint-plugin-import@2.x eslint-plugin-jsx-a11y@6.x eslint-plugin-react@7.x eslint-plugin-react-hooks@4.x @babel/eslint-parser@7.x @babel/core@7.x @babel/plugin-syntax-jsx@7.x @babel/preset-env@7.x @babel/preset-react@7.x 20 | [ -f .eslintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.eslintrc.json 21 | [ -f .babelrc ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.babelrc 22 | - name: ESLint Report 23 | run: npx eslint "**/*.{js,jsx}" 24 | stylelint: 25 | name: Stylelint 26 | runs-on: ubuntu-22.04 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-node@v1 30 | with: 31 | node-version: "18.x" 32 | - name: Setup Stylelint 33 | run: | 34 | npm install --save-dev stylelint@13.x stylelint-scss@3.x stylelint-config-standard@21.x stylelint-csstree-validator@1.x 35 | [ -f .stylelintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.stylelintrc.json 36 | - name: Stylelint Report 37 | run: npx stylelint "**/*.{css,scss}" 38 | nodechecker: 39 | name: node_modules checker 40 | runs-on: ubuntu-22.04 41 | steps: 42 | - uses: actions/checkout@v2 43 | - name: Check node_modules existence 44 | run: | 45 | if [ -d "node_modules/" ]; then echo -e "\e[1;31mThe node_modules/ folder was pushed to the repo. Please remove it from the GitHub repository and try again."; echo -e "\e[1;32mYou can set up a .gitignore file with this folder included on it to prevent this from happening in the future." && exit 1; fi 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | .history/ 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard"], 3 | "plugins": ["stylelint-scss", "stylelint-csstree-validator"], 4 | "rules": { 5 | "at-rule-no-unknown": [ 6 | true, 7 | { 8 | "ignoreAtRules": ["tailwind", "apply", "variants", "responsive", "screen"] 9 | } 10 | ], 11 | "scss/at-rule-no-unknown": [ 12 | true, 13 | { 14 | "ignoreAtRules": ["tailwind", "apply", "variants", "responsive", "screen"] 15 | } 16 | ], 17 | "csstree/validator": true 18 | }, 19 | "ignoreFiles": ["build/**", "dist/**", "**/reset*.css", "**/bootstrap*.css", "**/*.js", "**/*.jsx"] 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Salim Bamahfoodh 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 | 2 | 3 |
4 | logo 5 |
6 | 7 |

Welcome to my project 😃

8 | 9 |
10 | 11 | 12 |
13 | 14 |
15 |

Math Magicians

16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | # 📗 Table of Contents 25 | 26 | - [📗 Table of Contents](#-table-of-contents) 27 | - [📖 \[Math Magicians\] ](#-math-magicians-) 28 | - [🛠 Built With ](#-built-with-) 29 | - [Tech Stack ](#tech-stack-) 30 | - [Key Features ](#key-features-) 31 | - [🚀 Live Demo ](#-live-demo-) 32 | - [💻 Getting Started ](#-getting-started-) 33 | - [Prerequisites](#prerequisites) 34 | - [Setup](#setup) 35 | - [Install](#install) 36 | - [Usage](#usage) 37 | - [Run tests](#run-tests) 38 | - [👥 Author ](#-author-) 39 | - [👥 Collaborator ](#-collaborator-) 40 | - [🔭 Future Features ](#-future-features-) 41 | - [🤝 Contributing ](#-contributing-) 42 | - [⭐️ Show your support ](#️-show-your-support-) 43 | - [🙏 Acknowledgments ](#-acknowledgments-) 44 | - [📝 License ](#-license-) 45 | 46 | 47 | 48 | # 📖 [Math Magicians] 49 | 50 | 51 | **Math magicians** is a website for all fans of mathematics. It is a Single Page App (SPA) that allows users to: 52 | 53 | - Make simple calculations. 54 | - Read a random quote. 55 | 56 | By building this application, I improve my skills in using React. 57 | 58 | ## 🛠 Built With 59 | 60 | ### Tech Stack 61 | 62 |
63 | Client 64 | 70 |
71 | 72 | 73 | 74 | 75 | 76 | ### Key Features 77 | 78 | 79 | - **Easy to the eyes and cultivating at the same time** 80 | - **Dynamic creation of content** 81 | 82 |

(back to top)

83 | 84 | 93 | 94 | 95 | 96 | ## 🚀 Live Demo 97 | 98 | 99 | - [Live Demo Link](https://salimer.github.io/Math-magicians/) 100 | 101 |

(back to top)

102 | 103 | 104 | 105 | 106 | ## 💻 Getting Started 107 | 108 | To get a local copy up and running, follow these steps: 109 | 110 | - Clone this repo as described in the setup section. 111 | - Make modifications as preferred 112 | 113 | 114 | ### Prerequisites 115 | 116 | In order to run this project you need: To clone or fork and run it in a browser 117 | 118 | 119 | ### Setup 120 | 121 | Clone this repository to your desired folder: 122 | 123 | Example commands: 124 | 125 | ```sh 126 | cd my-folder 127 | git clone https://github.com/Salimer/Math-magicians.git 128 | ``` 129 | 130 | using Ubuntu: 131 | 132 | ```sh 133 | cd my-desired-folder 134 | git clone https://github.com/Salimer/Math-magicians.git 135 | ``` 136 | 137 | For more information on how to clone or fork a repository: 138 | - How to clone a repo 139 | - How to fork a repo 140 | 141 | ### Install 142 | 143 | - Run ` npm install ` 144 | 145 | ### Usage 146 | 147 | To run the project, execute the following command: 148 | 149 | - Run `npm start` 150 | 151 | ### Run tests 152 | 153 | To run tests, run the following command: 154 | 155 | - to test validation errors `npx hint .` 156 | 157 | - to test CSS linter errors `npx stylelint "**/*.{css,scss}"` 158 | 159 | - to test JavaScript linter errors `npx eslint .` 160 | 161 | - to run the jest tests `npx test` 162 | 163 |

(back to top)

164 | 165 | 166 | 167 | ## 👥 Author 168 | 169 | 👤 **Salim Bamahfoodh** 170 | 171 | 172 | - GitHub: [@Salimer](https://github.com/Salimer) 173 | 174 | ## 👥 Collaborator 175 | 176 | 👤 **Martín Ezequiel González** 177 | 178 | - GitHub: [@Mar12358](https://github.com/Mar12358) 179 | - Twitter: [@MarezegonZ](https://twitter.com/MarezegonZ) 180 | - LinkedIn: [Martin Ezequiel Gonzalez](https://www.linkedin.com/in/martin-ezequiel-gonzalez-30a413260/) 181 | 182 | 183 | 184 | 185 | 186 | ## 🔭 Future Features 187 | 188 | 189 | 190 | Future changes: 191 | - Add animations and transitions 192 | - Add more styles and colors 193 | 194 |

(back to top)

195 | 196 | 197 | 198 | ## 🤝 Contributing 199 | 200 | Contributions, issues, and feature requests are welcome! 201 | 202 | Feel free to check the [issues page](https://github.com/Salimer/Math-magicians/issues). 203 | 204 |

(back to top)

205 | 206 | 207 | 208 | ## ⭐️ Show your support 209 | 210 | 211 | Give a ⭐️ if you like this project! 212 | 213 |

(back to top)

214 | 215 | 216 | 217 | ## 🙏 Acknowledgments 218 | 219 | 220 | - Thanks to all Microverse community 221 | 222 | 223 |

(back to top)

224 | 225 | 242 | 243 | 244 | 245 | ## 📝 License 246 | 247 | This project is [MIT](./LICENSE) licensed. 248 | 249 | 250 |

(back to top)

251 | -------------------------------------------------------------------------------- /murple_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Salimer/Math-magicians/fdbe626b980c27cc3fdf57e8c5dc273816a3fb18/murple_logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "math-magicians", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/user-event": "^13.5.0", 8 | "big.js": "^6.2.1", 9 | "prop-types": "^15.8.1", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0", 12 | "react-router-dom": "^6.11.2", 13 | "react-scripts": "5.0.1", 14 | "sass": "^1.62.1", 15 | "web-vitals": "^2.1.4" 16 | }, 17 | "homepage": "https://Salimer.github.io/Math-magicians", 18 | "scripts": { 19 | "start": "PUBLIC_URL=/ react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject", 23 | "predeploy": "npm run build", 24 | "deploy": "gh-pages -d build" 25 | }, 26 | "eslintConfig": { 27 | "extends": [ 28 | "react-app", 29 | "react-app/jest" 30 | ] 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.22.1", 46 | "@babel/eslint-parser": "^7.21.8", 47 | "@babel/plugin-syntax-jsx": "^7.21.4", 48 | "@babel/preset-react": "^7.22.3", 49 | "@testing-library/react": "^14.0.0", 50 | "babel-jest": "^29.5.0", 51 | "eslint": "^7.32.0", 52 | "eslint-config-airbnb": "^18.2.1", 53 | "eslint-plugin-import": "^2.27.5", 54 | "eslint-plugin-jsx-a11y": "^6.7.1", 55 | "eslint-plugin-react": "^7.32.2", 56 | "eslint-plugin-react-hooks": "^4.6.0", 57 | "gh-pages": "^5.0.0", 58 | "stylelint": "^13.13.1", 59 | "stylelint-config-standard": "^21.0.0", 60 | "stylelint-csstree-validator": "^1.9.0", 61 | "stylelint-scss": "^3.21.0" 62 | }, 63 | "jest": { 64 | "transform": { 65 | "^.+\\.js$": "babel-jest" 66 | }, 67 | "moduleNameMapper": { 68 | "\\.(css|scss)$": "/src/__mocks__/styleMock.js" 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Salimer/Math-magicians/fdbe626b980c27cc3fdf57e8c5dc273816a3fb18/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Math Magicians 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | 36 | to { 37 | transform: rotate(360deg); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from 'react-router-dom'; 2 | 3 | import Home from './routes/Home'; 4 | import Quote from './routes/Quote'; 5 | import Calculate from './routes/Calculate'; 6 | import NotMatch from './routes/NotMatch'; 7 | import Layout from './components/Layout'; 8 | 9 | const App = () => ( 10 | 11 | }> 12 | } /> 13 | } /> 14 | } /> 15 | } /> 16 | 17 | 18 | ); 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /src/__tests__/Calculator.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, fireEvent } from '@testing-library/react'; 3 | import '@testing-library/jest-dom/extend-expect'; // Import extend-expect 4 | import Calculator from '../components/Calculator'; 5 | 6 | test('renders Calculator correctly', () => { 7 | const { container } = render(); 8 | expect(container).toMatchSnapshot(); 9 | }); 10 | 11 | test('updates calculator data correctly when a button is clicked', () => { 12 | const { getAllByText } = render(); 13 | 14 | fireEvent.click(getAllByText('2')[0]); // Select the first matching element 15 | expect(getAllByText('2')[0]).toBeInTheDocument(); 16 | 17 | fireEvent.click(getAllByText('+')[0]); 18 | expect(getAllByText('+')[0]).toBeInTheDocument(); 19 | 20 | fireEvent.click(getAllByText('5')[0]); 21 | expect(getAllByText('5')[0]).toBeInTheDocument(); 22 | 23 | fireEvent.click(getAllByText('=')[0]); 24 | expect(getAllByText('7')[0]).toBeInTheDocument(); 25 | 26 | fireEvent.click(getAllByText('-')[0]); 27 | expect(getAllByText('-')[0]).toBeInTheDocument(); 28 | 29 | fireEvent.click(getAllByText('3')[0]); 30 | expect(getAllByText('3')[0]).toBeInTheDocument(); 31 | 32 | fireEvent.click(getAllByText('+/-')[0]); 33 | expect(getAllByText('+/-')[0]).toBeInTheDocument(); 34 | }); 35 | -------------------------------------------------------------------------------- /src/__tests__/FetchQuote.test.js: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import FetchQuote from '../components/FetchQuote'; 3 | 4 | test('renders FetchQuote correctly', () => { 5 | const { conqatiner } = render( 6 | , 7 | ); 8 | expect(conqatiner).toMatchSnapshot(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/__tests__/Layout.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { MemoryRouter } from 'react-router-dom'; 4 | import Layout from '../components/Layout'; 5 | 6 | test('renders Layout correctly', () => { 7 | const { container } = render( 8 | 9 | 10 | , 11 | ); 12 | expect(container).toMatchSnapshot(); 13 | }); 14 | -------------------------------------------------------------------------------- /src/__tests__/Navbar.test.js: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | import Navbar from '../components/Navbar'; 4 | 5 | test('renders Navbar correctly', () => { 6 | const { container } = render( 7 | 8 | 9 | , 10 | ); 11 | expect(container).toMatchSnapshot(); 12 | }); 13 | -------------------------------------------------------------------------------- /src/__tests__/NumbersBtns.test.js: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import NumbersBtns from '../components/NumbersBtns'; 3 | 4 | test('renders numbers buttons correctly', () => { 5 | const handleButtonClick = jest.fn(); 6 | const { container } = render( 7 | , 8 | ); 9 | 10 | expect(container).toMatchSnapshot(); 11 | }); 12 | -------------------------------------------------------------------------------- /src/__tests__/OperatorBtns.test.js: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import OperatorBtns from '../components/OperatorBtns'; 3 | 4 | test('renders operator buttons correctly', () => { 5 | const handleButtonClick = jest.fn(); 6 | const { container } = render( 7 | , 8 | ); 9 | expect(container).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/Calculator.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders Calculator correctly 1`] = ` 4 |
5 |
8 |
11 | 0 12 |
13 |
16 | 22 | 28 | 34 | 40 | 46 |
47 |
50 | 56 | 62 | 68 | 74 | 80 | 86 | 92 | 98 | 104 | 110 | 116 | 122 | 128 | 134 |
135 |
136 |
137 | `; 138 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/FetchQuote.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders FetchQuote correctly 1`] = `undefined`; 4 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/Layout.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders Layout correctly 1`] = ` 4 |
5 |
8 | 44 |
45 |
46 | `; 47 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/Navbar.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders Navbar correctly 1`] = ` 4 |
5 | 41 |
42 | `; 43 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/NumbersBtns.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders numbers buttons correctly 1`] = ` 4 |
5 |
8 | 14 | 20 | 26 | 32 | 38 | 44 | 50 | 56 | 62 | 68 | 74 | 80 | 86 | 92 |
93 |
94 | `; 95 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/OperatorBtns.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders operator buttons correctly 1`] = ` 4 |
5 |
8 | 14 | 20 | 26 | 32 | 38 |
39 |
40 | `; 41 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/operate.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`operate function performs addition correctly 1`] = `"5"`; 4 | 5 | exports[`operate function performs division correctly 1`] = `"5"`; 6 | 7 | exports[`operate function performs multiplication correctly 1`] = `"24"`; 8 | 9 | exports[`operate function performs subtraction correctly 1`] = `"3"`; 10 | -------------------------------------------------------------------------------- /src/__tests__/calculate.test.js: -------------------------------------------------------------------------------- 1 | import calculate from '../logic/calculate'; 2 | 3 | describe('calculate function', () => { 4 | test("returns initial state when 'AC' button is clicked", () => { 5 | const result = calculate({ total: '5', next: '10', operation: '+' }, 'AC'); 6 | expect(result).toEqual({ total: null, next: null, operation: null }); 7 | }); 8 | 9 | test('handles decimal point correctly when "." button is clicked', () => { 10 | const result = calculate({ total: null, next: '5', operation: null }, '.'); 11 | expect(result).toEqual({ total: null, next: '5.', operation: null }); 12 | }); 13 | 14 | test('performs addition when "+" button is clicked', () => { 15 | const result = calculate({ total: '10', next: '5', operation: '+' }, '+'); 16 | expect(result).toEqual({ total: '15', next: null, operation: '+' }); 17 | }); 18 | 19 | test('performs subtraction when "-" button is clicked', () => { 20 | const result = calculate({ total: '10', next: '5', operation: '-' }, '-'); 21 | expect(result).toEqual({ total: '5', next: null, operation: '-' }); 22 | }); 23 | 24 | test('performs modulo when "%" button is clicked', () => { 25 | const result = calculate({ total: '10', next: '5', operation: '%' }, '%'); 26 | expect(result).toEqual({ total: '0', next: null, operation: '%' }); 27 | }); 28 | 29 | test('changes sign when "+/-" button is clicked', () => { 30 | const result = calculate({ total: null, next: '5', operation: null }, '+/-'); 31 | expect(result).toEqual({ total: null, next: '-5', operation: null }); 32 | }); 33 | 34 | test('performs equality when "=" button is clicked', () => { 35 | const result = calculate({ total: '10', next: '5', operation: '+' }, '='); 36 | expect(result).toEqual({ total: '15', next: null, operation: null }); 37 | }); 38 | 39 | test('Append "0" when it is clicked', () => { 40 | const result = calculate({ total: '10', next: '5', operation: '+' }, '0'); 41 | expect(result).toEqual({ total: '10', next: '50', operation: '+' }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/__tests__/operate.test.js: -------------------------------------------------------------------------------- 1 | import operate from '../logic/operate'; 2 | 3 | describe('operate function', () => { 4 | test('performs addition correctly', () => { 5 | const result = operate('2', '3', '+'); 6 | expect(result).toMatchSnapshot(); 7 | }); 8 | 9 | test('performs subtraction correctly', () => { 10 | const result = operate('5', '2', '-'); 11 | expect(result).toMatchSnapshot(); 12 | }); 13 | 14 | test('performs multiplication correctly', () => { 15 | const result = operate('4', '6', 'x'); 16 | expect(result).toMatchSnapshot(); 17 | }); 18 | 19 | test('performs division correctly', () => { 20 | const result = operate('10', '2', '÷'); 21 | expect(result).toMatchSnapshot(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/components/Calculator.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import calculate from '../logic/calculate'; 3 | import OperatorBtns from './OperatorBtns'; 4 | import NumbersBtns from './NumbersBtns'; 5 | 6 | function Calculator() { 7 | const [calculatorData, setCalculatorData] = useState({ 8 | total: null, 9 | next: null, 10 | operation: null, 11 | }); 12 | 13 | const handleButtonClick = (event) => { 14 | const clicked = event.target.textContent; 15 | const value = calculate(calculatorData, clicked); 16 | setCalculatorData(value); 17 | }; 18 | 19 | const { total, operation, next } = calculatorData; 20 | return ( 21 |
22 |
{ next || operation || total || 0 }
23 | 24 | 25 |
26 | ); 27 | } 28 | 29 | export default Calculator; 30 | -------------------------------------------------------------------------------- /src/components/FetchQuote.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | const FetchQuote = () => { 4 | const [data, setData] = useState([]); 5 | const [loading, setLoading] = useState(false); 6 | const [error, setError] = useState(false); 7 | 8 | useEffect(() => { 9 | const fetchData = async () => { 10 | try { 11 | setLoading(true); 12 | const response = await fetch('https://api.api-ninjas.com/v1/quotes?category=alone', { 13 | headers: { 14 | 'X-Api-Key': 'vZdI6eGz2cS+jTb71dvouA==X8Xd3lMbKviCRmqW', 15 | }, 16 | }); 17 | if (!response.ok) { 18 | throw new Error('Request failed'); 19 | } 20 | const fetchedData = await response.json(); 21 | setData(fetchedData); 22 | setLoading(false); 23 | } catch (error) { 24 | setError(true); 25 | setLoading(false); 26 | } 27 | }; 28 | 29 | fetchData(); 30 | }, []); 31 | 32 | if (error) return
Something went wrong!
; 33 | 34 | if (loading) return
Loading...
; 35 | 36 | return ( 37 |
    38 | {data.map((item) => ( 39 |
  • 40 | {item.quote} 41 | {' '} 42 |
    43 |
    44 | - 45 | {item.author} 46 |
  • 47 | ))} 48 |
49 | ); 50 | }; 51 | 52 | export default FetchQuote; 53 | -------------------------------------------------------------------------------- /src/components/Layout.js: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router-dom'; 2 | import Navbar from './Navbar'; 3 | 4 | const Layout = () => ( 5 |
6 | 7 | 8 |
9 | ); 10 | export default Layout; 11 | -------------------------------------------------------------------------------- /src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import { NavLink } from 'react-router-dom'; 2 | 3 | const links = [ 4 | { path: '/', text: 'Home' }, 5 | { path: '/calculate', text: 'Calculate' }, 6 | { path: '/quote', text: 'Quote' }, 7 | ]; 8 | const Navbar = () => ( 9 | 19 | ); 20 | export default Navbar; 21 | -------------------------------------------------------------------------------- /src/components/NumbersBtns.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | function NumbersBtns({ handleButtonClick }) { 4 | return ( 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | ); 22 | } 23 | 24 | NumbersBtns.propTypes = { handleButtonClick: PropTypes.func.isRequired }; 25 | 26 | export default NumbersBtns; 27 | -------------------------------------------------------------------------------- /src/components/OperatorBtns.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | function OperatorBtns({ handleButtonClick }) { 4 | return ( 5 |
6 | 7 | 8 | 9 | 10 | 11 |
12 | ); 13 | } 14 | 15 | OperatorBtns.propTypes = { handleButtonClick: PropTypes.func.isRequired }; 16 | 17 | export default OperatorBtns; 18 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 4 | -apple-system, 5 | BlinkMacSystemFont, 6 | 'Segoe UI', 7 | 'Roboto', 8 | 'Oxygen', 9 | 'Ubuntu', 10 | 'Cantarell', 11 | 'Fira Sans', 12 | 'Droid Sans', 13 | 'Helvetica Neue', 14 | sans-serif; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | } 18 | 19 | code { 20 | font-family: 21 | source-code-pro, 22 | Menlo, 23 | Monaco, 24 | Consolas, 25 | 'Courier New', 26 | monospace; 27 | } 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import './index.css'; 5 | import App from './App'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | 13 | , 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | -------------------------------------------------------------------------------- /src/logic/calculate.js: -------------------------------------------------------------------------------- 1 | import operate from './operate'; 2 | 3 | function isNumber(item) { 4 | return !!item.match(/[0-9]+/); 5 | } 6 | 7 | /** 8 | * Given a button name and a calculator data object, return an updated 9 | * calculator data object. 10 | * 11 | * Calculator data object contains: 12 | * total:s the running total 13 | * next:String the next number to be operated on with the total 14 | * operation:String +, -, etc. 15 | */ 16 | export default function calculate(obj, buttonName) { 17 | if (buttonName === 'AC') { 18 | return { 19 | total: null, 20 | next: null, 21 | operation: null, 22 | }; 23 | } 24 | 25 | if (isNumber(buttonName)) { 26 | if (buttonName === '0' && obj.next === '0') { 27 | return {}; 28 | } 29 | // If there is an operation, update next 30 | if (obj.operation) { 31 | if (obj.next && obj.next !== '0') { 32 | return { ...obj, next: obj.next + buttonName }; 33 | } 34 | return { ...obj, next: buttonName }; 35 | } 36 | // If there is no operation, update next and clear the value 37 | if (obj.next && obj.next !== '0') { 38 | return { 39 | next: obj.next + buttonName, 40 | total: null, 41 | }; 42 | } 43 | return { 44 | next: buttonName, 45 | total: null, 46 | }; 47 | } 48 | 49 | if (buttonName === '.') { 50 | if (obj.next) { 51 | if (obj.next.includes('.')) { 52 | return { ...obj }; 53 | } 54 | return { ...obj, next: `${obj.next}.` }; 55 | } 56 | if (obj.operation) { 57 | return { ...obj, next: '0.' }; 58 | } 59 | if (obj.total) { 60 | if (obj.total.includes('.')) { 61 | return {}; 62 | } 63 | return { ...obj, next: `${obj.total}.` }; 64 | } 65 | return { ...obj, next: '0.' }; 66 | } 67 | 68 | if (buttonName === '=') { 69 | if (obj.next && obj.operation) { 70 | return { 71 | total: operate(obj.total, obj.next, obj.operation), 72 | next: null, 73 | operation: null, 74 | }; 75 | } 76 | // '=' with no operation, nothing to do 77 | return {}; 78 | } 79 | 80 | if (buttonName === '+/-') { 81 | if (obj.next) { 82 | return { ...obj, next: (-1 * parseFloat(obj.next)).toString() }; 83 | } 84 | if (obj.total) { 85 | return { ...obj, total: (-1 * parseFloat(obj.total)).toString() }; 86 | } 87 | return {}; 88 | } 89 | 90 | // Button must be an operation 91 | 92 | // When the user presses an operation button without having entered 93 | // a number first, do nothing. 94 | // if (!obj.next && !obj.total) { 95 | // return {}; 96 | // } 97 | 98 | // User pressed an operation after pressing '=' 99 | if (!obj.next && obj.total && !obj.operation) { 100 | return { ...obj, operation: buttonName }; 101 | } 102 | 103 | // User pressed an operation button and there is an existing operation 104 | if (obj.operation) { 105 | if (obj.total && !obj.next) { 106 | return { ...obj, operation: buttonName }; 107 | } 108 | 109 | if (!obj.total) { 110 | return { total: 0, operation: buttonName }; 111 | } 112 | 113 | return { 114 | total: operate(obj.total, obj.next, obj.operation), 115 | next: null, 116 | operation: buttonName, 117 | }; 118 | } 119 | 120 | // no operation yet, but the user typed one 121 | 122 | // The user hasn't typed a number yet, just save the operation 123 | if (!obj.next) { 124 | return { operation: buttonName }; 125 | } 126 | 127 | // save the operation and shift 'next' into 'total' 128 | return { 129 | total: obj.next, 130 | next: null, 131 | operation: buttonName, 132 | }; 133 | } 134 | -------------------------------------------------------------------------------- /src/logic/operate.js: -------------------------------------------------------------------------------- 1 | import Big from 'big.js'; 2 | 3 | export default function operate(numberOne, numberTwo, operation) { 4 | const one = Big(numberOne); 5 | const two = Big(numberTwo); 6 | if (operation === '+') { 7 | return one.plus(two).toString(); 8 | } 9 | if (operation === '-') { 10 | return one.minus(two).toString(); 11 | } 12 | if (operation === 'x') { 13 | return one.times(two).toString(); 14 | } 15 | if (operation === '÷') { 16 | try { 17 | return one.div(two).toString(); 18 | } catch (err) { 19 | return "Can't divide by 0."; 20 | } 21 | } 22 | if (operation === '%') { 23 | try { 24 | return one.mod(two).toString(); 25 | } catch (err) { 26 | return "Can't find modulo as can't divide by 0."; 27 | } 28 | } 29 | throw Error(`Unknown operation '${operation}'`); 30 | } 31 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/routes/Calculate.js: -------------------------------------------------------------------------------- 1 | import '../App.css'; 2 | import Calculator from '../components/Calculator'; 3 | 4 | function Calculate() { 5 | return ( 6 |
7 |

Let us do some math!

8 | 9 |
10 | ); 11 | } 12 | 13 | export default Calculate; 14 | -------------------------------------------------------------------------------- /src/routes/Home.js: -------------------------------------------------------------------------------- 1 | import '../styles/style.scss'; 2 | 3 | const Home = () => ( 4 |
5 |

Welcome to our page!

6 |

7 | lorem ipsum dolor sit amet, consectetur adipiscing elit, 8 | sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 9 | lorem ipsum dolor sit amet, consectetur adipiscing elit, 10 | sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 11 | lorem ipsum dolor sit amet, consectetur adipiscing elit, 12 | sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 13 |

14 |
15 | ); 16 | export default Home; 17 | -------------------------------------------------------------------------------- /src/routes/NotMatch.js: -------------------------------------------------------------------------------- 1 | const NotMatch = () => ( 2 |
3 |

Not found!

4 |
5 | ); 6 | export default NotMatch; 7 | -------------------------------------------------------------------------------- /src/routes/Quote.js: -------------------------------------------------------------------------------- 1 | import FetchQuote from '../components/FetchQuote'; 2 | 3 | const Quote = () => ( 4 |
5 | 6 |
7 | ); 8 | export default Quote; 9 | -------------------------------------------------------------------------------- /src/styles/style.scss: -------------------------------------------------------------------------------- 1 | 2 | ul { 3 | list-style: none; 4 | margin: 0; 5 | padding: 0; 6 | 7 | li { 8 | margin-bottom: 0; 9 | } 10 | } 11 | 12 | a { 13 | color: #fff; 14 | text-decoration: none; 15 | } 16 | 17 | .navbar { 18 | display: flex; 19 | justify-content: space-between; 20 | align-items: center; 21 | padding: 1rem; 22 | background-color: #f4913e; 23 | color: #fff; 24 | 25 | .navigation { 26 | display: flex; 27 | justify-content: space-between; 28 | align-items: center; 29 | 30 | li { 31 | padding: 0 0.5rem; 32 | } 33 | 34 | li + li { 35 | border-left: 1px solid #fff; 36 | } 37 | } 38 | } 39 | 40 | .calculate-page { 41 | display: flex; 42 | justify-content: center; 43 | align-items: flex-start; 44 | gap: 2rem; 45 | height: 100vh; 46 | background-color: #f5f5f5; 47 | padding: 1rem; 48 | 49 | a { 50 | color: #333; 51 | text-decoration: none; 52 | } 53 | 54 | /* Active route */ 55 | .active { 56 | color: #005bb3; 57 | } 58 | 59 | .calculator-container { 60 | display: grid; 61 | grid-template-areas: 62 | "screen screen screen screen" 63 | "numbers numbers numbers operator" 64 | "numbers numbers numbers operator" 65 | "numbers numbers numbers operator" 66 | "numbers numbers numbers operator" 67 | "numbers numbers numbers operator"; 68 | 69 | .screen { 70 | grid-area: screen; 71 | color: #fff; 72 | font-size: 2rem; 73 | text-align: right; 74 | padding: 1rem 0.5rem; 75 | background-color: #858693; 76 | } 77 | 78 | .numbers { 79 | grid-area: numbers; 80 | display: grid; 81 | grid-template-columns: repeat(3, 1fr); 82 | background-color: #e0e0e0; 83 | 84 | button:nth-child(13) { 85 | grid-column: span 2; 86 | } 87 | 88 | .button { 89 | text-align: center; 90 | border: 1px solid #bababa; 91 | padding: 1rem; 92 | } 93 | } 94 | 95 | .operator { 96 | grid-area: operator; 97 | display: grid; 98 | grid-template-columns: repeat(1, 1fr); 99 | 100 | .button { 101 | text-align: center; 102 | border: 1px solid #bababa; 103 | padding: 1rem; 104 | background-color: #f4913e; 105 | } 106 | } 107 | } 108 | 109 | .quote { 110 | font-size: 1.5rem; 111 | text-align: center; 112 | margin-top: 1rem; 113 | color: #858693; 114 | max-width: 20rem; 115 | 116 | .li { 117 | list-style: none; 118 | margin-bottom: 1rem; 119 | } 120 | } 121 | } 122 | 123 | .home-page { 124 | padding: 10%; 125 | } 126 | 127 | .quote-page { 128 | padding: 10%; 129 | } 130 | --------------------------------------------------------------------------------