├── .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 |
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 | You need to enable JavaScript to run this app.
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 |
20 | ÷
21 |
22 |
26 | x
27 |
28 |
32 | -
33 |
34 |
38 | +
39 |
40 |
44 | =
45 |
46 |
47 |
50 |
54 | AC
55 |
56 |
60 | +/-
61 |
62 |
66 | %
67 |
68 |
72 | 7
73 |
74 |
78 | 8
79 |
80 |
84 | 9
85 |
86 |
90 | 4
91 |
92 |
96 | 5
97 |
98 |
102 | 6
103 |
104 |
108 | 1
109 |
110 |
114 | 2
115 |
116 |
120 | 3
121 |
122 |
126 | 0
127 |
128 |
132 | .
133 |
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 |
11 |
12 | Math Magicians
13 |
14 |
43 |
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 |
8 |
9 | Math Magicians
10 |
11 |
40 |
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 |
12 | AC
13 |
14 |
18 | +/-
19 |
20 |
24 | %
25 |
26 |
30 | 7
31 |
32 |
36 | 8
37 |
38 |
42 | 9
43 |
44 |
48 | 4
49 |
50 |
54 | 5
55 |
56 |
60 | 6
61 |
62 |
66 | 1
67 |
68 |
72 | 2
73 |
74 |
78 | 3
79 |
80 |
84 | 0
85 |
86 |
90 | .
91 |
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 |
12 | ÷
13 |
14 |
18 | x
19 |
20 |
24 | -
25 |
26 |
30 | +
31 |
32 |
36 | =
37 |
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 |
10 | Math Magicians
11 |
12 | {links.map((link) => (
13 |
14 | {link.text}
15 |
16 | ))}
17 |
18 |
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 | AC
7 | +/-
8 | %
9 | 7
10 | 8
11 | 9
12 | 4
13 | 5
14 | 6
15 | 1
16 | 2
17 | 3
18 | 0
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 | x
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 |
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 |
--------------------------------------------------------------------------------