├── __mocks__ ├── styleMock.js └── fileMock.js ├── .hound.yml ├── src ├── scss │ ├── app.scss │ ├── _variables.scss │ ├── base.scss │ ├── githubIcon.scss │ ├── home.scss │ ├── soundIcon.scss │ ├── fonts.scss │ ├── calculator.scss │ ├── button.scss │ └── app-viewport-mobile.scss ├── favicon.ico ├── sounds │ └── input.mp3 ├── fonts │ ├── geosanslight.woff │ ├── geosanslight.woff2 │ ├── rounded_elegance.woff │ └── rounded_elegance.woff2 ├── images │ ├── calctwitter.png │ ├── calcgoogleplus.png │ ├── favicons │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-76x76.png │ │ └── safari-pinned-tab.svg │ ├── icons │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ ├── icon-512x512.png │ │ ├── icon-72x72.png │ │ └── icon-96x96.png │ └── speaker.svg ├── scripts │ ├── actions │ │ ├── createAction.js │ │ └── constants.js │ ├── reducers │ │ ├── muted.js │ │ ├── keyDown.js │ │ ├── calculated.js │ │ ├── clear.js │ │ ├── del.js │ │ ├── comma.js │ │ ├── historyDisplay.js │ │ ├── switchOperator.js │ │ ├── root.js │ │ ├── percent.js │ │ ├── displayValue.js │ │ ├── operator.js │ │ ├── calc.js │ │ ├── history.js │ │ └── add.js │ ├── model │ │ ├── initialState.js │ │ ├── sound.js │ │ ├── helper.js │ │ └── mapKeys.js │ ├── components │ │ ├── display.js │ │ ├── githubIcon.js │ │ ├── button.js │ │ └── soundIcon.js │ ├── app.js │ ├── views │ │ ├── calculator.js │ │ └── home.js │ └── container │ │ └── appContainer.js ├── browserconfig.xml ├── manifest.webmanifest ├── GzipSimpleHTTPServer.py └── index.html ├── public ├── favicon.ico ├── sounds │ └── input.mp3 ├── fonts │ ├── geosanslight.woff │ ├── geosanslight.woff2 │ ├── rounded_elegance.woff │ └── rounded_elegance.woff2 ├── images │ ├── calctwitter.png │ ├── calcgoogleplus.png │ ├── favicons │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ └── safari-pinned-tab.svg │ ├── icons │ │ ├── icon-72x72.png │ │ ├── icon-96x96.png │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ └── icon-512x512.png │ └── speaker.svg ├── browserconfig.xml ├── manifest.webmanifest ├── sw.js ├── sw.js.map ├── workbox-69b5a3b7.js ├── index.html └── .htaccess ├── .vscode └── settings.json ├── postcss.config.js ├── .gitignore ├── workbox-config.js ├── spec ├── dataFixture.js ├── del.spec.js ├── clear.spec.js ├── comma.spec.js ├── percent.spec.js ├── calculated.spec.js ├── historyDisplay.spec.js ├── add.spec.js ├── history.spec.js ├── calc.spec.js ├── button.spec.js ├── displayValue.spec.js └── helper.spec.js ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── deploy.yml ├── .stylelintrc ├── .babelrc ├── .codeclimate.yml ├── webpack.dev.js ├── .editorconfig ├── runtime.webpack.config.js ├── .eslintrc ├── README.md ├── webpack.prod.js ├── webpack.common.js └── package.json /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | eslint: 2 | enabled: true 3 | config_file: .eslintrc 4 | -------------------------------------------------------------------------------- /src/scss/app.scss: -------------------------------------------------------------------------------- 1 | @import 'base'; 2 | @import 'app-viewport-mobile'; 3 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.renderWhitespace": "all" 4 | } 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-cssnext': {} 4 | } 5 | } -------------------------------------------------------------------------------- /src/sounds/input.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/sounds/input.mp3 -------------------------------------------------------------------------------- /public/sounds/input.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/sounds/input.mp3 -------------------------------------------------------------------------------- /src/fonts/geosanslight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/fonts/geosanslight.woff -------------------------------------------------------------------------------- /src/fonts/geosanslight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/fonts/geosanslight.woff2 -------------------------------------------------------------------------------- /src/images/calctwitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/calctwitter.png -------------------------------------------------------------------------------- /public/fonts/geosanslight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/fonts/geosanslight.woff -------------------------------------------------------------------------------- /public/images/calctwitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/calctwitter.png -------------------------------------------------------------------------------- /src/images/calcgoogleplus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/calcgoogleplus.png -------------------------------------------------------------------------------- /public/fonts/geosanslight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/fonts/geosanslight.woff2 -------------------------------------------------------------------------------- /public/images/calcgoogleplus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/calcgoogleplus.png -------------------------------------------------------------------------------- /src/fonts/rounded_elegance.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/fonts/rounded_elegance.woff -------------------------------------------------------------------------------- /src/fonts/rounded_elegance.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/fonts/rounded_elegance.woff2 -------------------------------------------------------------------------------- /src/images/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/favicons/favicon.ico -------------------------------------------------------------------------------- /src/images/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/images/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/icons/icon-144x144.png -------------------------------------------------------------------------------- /src/images/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/icons/icon-152x152.png -------------------------------------------------------------------------------- /src/images/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/images/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/images/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/images/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/images/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/icons/icon-96x96.png -------------------------------------------------------------------------------- /public/fonts/rounded_elegance.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/fonts/rounded_elegance.woff -------------------------------------------------------------------------------- /public/fonts/rounded_elegance.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/fonts/rounded_elegance.woff2 -------------------------------------------------------------------------------- /public/images/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/favicons/favicon.ico -------------------------------------------------------------------------------- /public/images/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/icons/icon-72x72.png -------------------------------------------------------------------------------- /public/images/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/icons/icon-96x96.png -------------------------------------------------------------------------------- /public/images/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/icons/icon-128x128.png -------------------------------------------------------------------------------- /public/images/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/icons/icon-144x144.png -------------------------------------------------------------------------------- /public/images/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/icons/icon-152x152.png -------------------------------------------------------------------------------- /public/images/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/icons/icon-192x192.png -------------------------------------------------------------------------------- /public/images/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/icons/icon-384x384.png -------------------------------------------------------------------------------- /public/images/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/images/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /src/images/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /src/images/favicons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/favicons/mstile-144x144.png -------------------------------------------------------------------------------- /src/images/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/ 3 | coverage/ 4 | nodesource_setup.sh 5 | testing 6 | src/GzipSimpleHTTPServer.py 7 | debug.log 8 | -------------------------------------------------------------------------------- /public/images/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /public/images/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /src/images/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/images/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/images/favicons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/favicons/mstile-144x144.png -------------------------------------------------------------------------------- /public/images/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /src/images/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/images/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/images/favicons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/favicons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /src/images/favicons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/favicons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /src/images/favicons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/favicons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /src/images/favicons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/favicons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /src/images/favicons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/src/images/favicons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /public/images/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/images/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/images/favicons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/favicons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /public/images/favicons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/favicons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /public/images/favicons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/favicons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /public/images/favicons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/favicons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /public/images/favicons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iondrimba/react-calculator/HEAD/public/images/favicons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /workbox-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | swDest: 'public/sw.js', 3 | globPatterns: ['**/*.{js,png,html,css,woff2,woff,svg}'], 4 | globDirectory: './public' 5 | } 6 | -------------------------------------------------------------------------------- /src/scripts/actions/createAction.js: -------------------------------------------------------------------------------- 1 | const createAction = (type, ...args) => { 2 | return Object.assign({}, { type }, ...args); 3 | }; 4 | 5 | export default createAction; 6 | -------------------------------------------------------------------------------- /spec/dataFixture.js: -------------------------------------------------------------------------------- 1 | const dataFixture = { 2 | displayValue: '', 3 | historyDisplay: '', 4 | history: [], 5 | calculated: false 6 | }; 7 | 8 | export default dataFixture; 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "09:00" 8 | open-pull-requests-limit: 1 9 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "indentation": 2, 5 | "no-missing-end-of-source-newline": false, 6 | "number-leading-zero":"never" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/react" 5 | ], 6 | "sourceMap": "inline", 7 | "env": { 8 | "testing": { 9 | "sourceMap": "inline" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | exclude_patterns: 2 | - "**/node_modules/" 3 | - "**/spec/" 4 | - "**/public/" 5 | - "**/*.py" 6 | - "workbox-config.js" 7 | - "webpack.config.js" 8 | - "runtime.webpack.config.js" 9 | - "postcss.config.js" 10 | -------------------------------------------------------------------------------- /src/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | $button-shadow: rgba(0, 0, 0, .3); 2 | $primary-color: #a9133d; 3 | $air: #fff; 4 | $secondary-color: #1f2525; 5 | $base-color: #333; 6 | $button-base-color: #1f2525; 7 | $border-color: rgba(255, 255, 255, .04); 8 | -------------------------------------------------------------------------------- /src/scss/base.scss: -------------------------------------------------------------------------------- 1 | @import 'fonts'; 2 | @import './variables'; 3 | 4 | * { 5 | box-sizing: border-box; 6 | } 7 | 8 | button { 9 | font-family: inherit; 10 | margin: 0; 11 | } 12 | 13 | a { 14 | color: $air; 15 | } 16 | -------------------------------------------------------------------------------- /src/scripts/reducers/muted.js: -------------------------------------------------------------------------------- 1 | import { MUTED } from '../actions/constants'; 2 | 3 | function muted(state = false, action) { 4 | switch (action.type) { 5 | case MUTED: 6 | return action.value; 7 | 8 | } 9 | 10 | return state; 11 | } 12 | 13 | export default muted; 14 | -------------------------------------------------------------------------------- /src/scripts/reducers/keyDown.js: -------------------------------------------------------------------------------- 1 | import { KEY_DOWN } from '../actions/constants'; 2 | 3 | function keyDown(state = '', action) { 4 | switch (action.type) { 5 | case KEY_DOWN: 6 | return action.value; 7 | } 8 | 9 | return state; 10 | } 11 | 12 | export default keyDown; 13 | -------------------------------------------------------------------------------- /src/scripts/reducers/calculated.js: -------------------------------------------------------------------------------- 1 | import { CALCULATED } from '../actions/constants'; 2 | 3 | function calculated(state = false, action) { 4 | switch (action.type) { 5 | case CALCULATED: 6 | return action.value; 7 | } 8 | return state; 9 | } 10 | 11 | export default calculated; 12 | -------------------------------------------------------------------------------- /src/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #1f2525 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/scripts/model/initialState.js: -------------------------------------------------------------------------------- 1 | import MapKeys from './mapKeys'; 2 | 3 | const defaultStore = { 4 | historyDisplay: '', 5 | history: [], 6 | displayValue: '0', 7 | keyDown: '', 8 | muted: false, 9 | calculated: false, 10 | keys: MapKeys 11 | }; 12 | 13 | export default defaultStore; 14 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #1f2525 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/scripts/reducers/clear.js: -------------------------------------------------------------------------------- 1 | import { CLEAR } from '../actions/constants'; 2 | 3 | function clear(state = '0', action) { 4 | switch (action.type) { 5 | case CLEAR: 6 | if (action.value) { 7 | return '0'; 8 | } 9 | } 10 | 11 | return state; 12 | } 13 | 14 | export default clear; 15 | -------------------------------------------------------------------------------- /src/scripts/model/sound.js: -------------------------------------------------------------------------------- 1 | import Howler from 'howler'; 2 | 3 | class Sound { 4 | setup() { 5 | this.sound = new Howler.Howl({ 6 | src: './sounds/input.mp3' 7 | }); 8 | } 9 | 10 | play() { 11 | this.sound.play(); 12 | } 13 | 14 | mute(mute) { 15 | this.sound.mute(mute); 16 | } 17 | } 18 | 19 | export default Sound; 20 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const path = require('path'); 3 | const common = require('./webpack.common.js'); 4 | 5 | module.exports = merge(common, { 6 | mode: 'development', 7 | output: { 8 | publicPath: 'http://localhost:8080/', 9 | }, 10 | devServer: { 11 | contentBase: path.join(__dirname, 'public') 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.{diff,md}] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /src/scss/githubIcon.scss: -------------------------------------------------------------------------------- 1 | @import './variables'; 2 | 3 | .github { 4 | display: none; 5 | fill: $air; 6 | left: 50%; 7 | margin-top: 50px; 8 | position: relative; 9 | transform: translate(0, -50%); 10 | 11 | span { 12 | visibility: hidden; 13 | } 14 | 15 | &:active { 16 | left: 50.1%; 17 | top: 1px; 18 | } 19 | 20 | &:hover { 21 | fill: #a9b9f1; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/scripts/components/display.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class Display extends PureComponent { 5 | render() { 6 | return ( 7 |

{this.props.value}

8 | ); 9 | } 10 | } 11 | 12 | Display.propTypes = { 13 | value: PropTypes.string, 14 | className:PropTypes.string, 15 | }; 16 | 17 | export default Display; 18 | -------------------------------------------------------------------------------- /src/scss/home.scss: -------------------------------------------------------------------------------- 1 | .home { 2 | left: 50%; 3 | opacity: 0; 4 | position: absolute; 5 | top: 50%; 6 | transform: translateY(-50%) translateX(-50%); 7 | 8 | &__content { 9 | border-radius: 8px; 10 | box-shadow: 0 6px 17px 0 rgba(0, 0, 0, .21); 11 | height: 88vh; 12 | margin: 0 auto; 13 | overflow: hidden; 14 | width: 95vw; 15 | } 16 | } 17 | 18 | .fadeIn { 19 | opacity: 1; 20 | transition: opacity .3s .2s; 21 | } 22 | -------------------------------------------------------------------------------- /src/scripts/reducers/del.js: -------------------------------------------------------------------------------- 1 | import { DEL } from '../actions/constants'; 2 | import helper from '../model/helper'; 3 | 4 | function del(state = '0', action) { 5 | let output = '0'; 6 | 7 | switch (action.type) { 8 | case DEL: 9 | output = helper.removeLastChar(state); 10 | if (helper.isNaN(output) || helper.isNumberZero(output)) { 11 | output = '0'; 12 | } 13 | 14 | return output; 15 | } 16 | 17 | return state; 18 | } 19 | 20 | export default del; 21 | -------------------------------------------------------------------------------- /runtime.webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | output: { 3 | // YOU NEED TO SET libraryTarget: 'commonjs2' 4 | libraryTarget: 'commonjs2', 5 | }, 6 | 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.scss$/, 11 | loaders: [ 12 | 'style-loader', 13 | 'css-loader?sourceMap&modules&importLoaders=1&localIdentName=[local]', 14 | 'sass-loader', 15 | 'postcss-loader', 16 | ], 17 | }, 18 | ], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/scripts/reducers/comma.js: -------------------------------------------------------------------------------- 1 | import { COMMA } from '../actions/constants'; 2 | 3 | function comma(state = '', action) { 4 | let output = state; 5 | 6 | switch (action.type) { 7 | case COMMA: 8 | if (action.data.calculated) { 9 | output = `0${action.value}`; 10 | } else if (state.indexOf(',') === -1) { 11 | output = `${state}${action.value}`; 12 | } 13 | 14 | return output; 15 | } 16 | 17 | return state; 18 | } 19 | 20 | export default comma; 21 | -------------------------------------------------------------------------------- /src/scss/soundIcon.scss: -------------------------------------------------------------------------------- 1 | .soundIcon { 2 | cursor: pointer; 3 | fill: #1f2525; 4 | height: 22px; 5 | left: 10px; 6 | position: absolute; 7 | top: 10px; 8 | width: 25px; 9 | z-index: 1; 10 | 11 | &__wave { 12 | opacity: 1; 13 | } 14 | 15 | &__stripe { 16 | opacity: 0; 17 | } 18 | 19 | &--muted { 20 | .soundIcon__wave { 21 | opacity: 0; 22 | } 23 | 24 | .soundIcon__stripe { 25 | opacity: 1; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/scripts/reducers/historyDisplay.js: -------------------------------------------------------------------------------- 1 | import { CLEAR, OPERATOR, PERCENT, HISTORY_CLEAR } from '../actions/constants'; 2 | import operator from '../reducers/operator'; 3 | 4 | function historyDisplay(state = '', action) { 5 | switch (action.type) { 6 | case CLEAR: 7 | case HISTORY_CLEAR: 8 | case PERCENT: 9 | return ''; 10 | case OPERATOR: 11 | return operator(action.data.historyDisplay, action); 12 | } 13 | 14 | return state; 15 | } 16 | 17 | export default historyDisplay; 18 | -------------------------------------------------------------------------------- /src/images/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/speaker.svg: -------------------------------------------------------------------------------- 1 | speaker -------------------------------------------------------------------------------- /public/images/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/speaker.svg: -------------------------------------------------------------------------------- 1 | speaker -------------------------------------------------------------------------------- /src/scripts/actions/constants.js: -------------------------------------------------------------------------------- 1 | export const ADD = 'ADD'; 2 | export const KEY_DOWN = 'KEY_DOWN'; 3 | export const KEY_UP = 'KEY_UP'; 4 | export const CALC = 'CALC'; 5 | export const CLEAR = 'CLEAR'; 6 | export const DEL = 'DEL'; 7 | export const OPERATOR = 'OPERATOR'; 8 | export const CALCULATED = 'CALCULATED'; 9 | export const COMMA = 'COMMA'; 10 | export const SWITCH_OPERATOR = 'SWITCH_OPERATOR'; 11 | export const PERCENT = 'PERCENT'; 12 | export const MUTED = 'MUTED'; 13 | export const HISTORY_CLEAR = 'HISTORY_CLEAR'; 14 | -------------------------------------------------------------------------------- /src/scripts/reducers/switchOperator.js: -------------------------------------------------------------------------------- 1 | import { SWITCH_OPERATOR } from '../actions/constants'; 2 | import helper from '../model/helper'; 3 | 4 | function switchOperator(state = '', action) { 5 | let output = ''; 6 | 7 | switch (action.type) { 8 | case SWITCH_OPERATOR: 9 | if (helper.isPositiveNumber(state)) { 10 | output = `-${state}`; 11 | } else { 12 | output = state.replace(/-/, ''); 13 | } 14 | 15 | return output; 16 | } 17 | 18 | return state; 19 | } 20 | 21 | export default switchOperator; 22 | -------------------------------------------------------------------------------- /src/scss/fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'geosanslightregular'; 3 | font-style: normal; 4 | font-weight: normal; 5 | src: url('../fonts/geosanslight.woff2') format('woff2'), url('../fonts/geosanslight.woff') format('woff'); 6 | font-display: swap; 7 | } 8 | 9 | @font-face { 10 | font-family: 'rounded_eleganceregular'; 11 | font-style: normal; 12 | font-weight: normal; 13 | src: url('../fonts/rounded_elegance.woff2') format('woff2'), url('../fonts/rounded_elegance.woff') format('woff'); 14 | font-display: swap; 15 | } 16 | -------------------------------------------------------------------------------- /src/scripts/reducers/root.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import historyDisplay from './historyDisplay'; 3 | import history from './history'; 4 | import displayValue from './displayValue'; 5 | import MapKeys from '../model/mapKeys'; 6 | import keyDown from './keyDown'; 7 | import calculated from './calculated'; 8 | import muted from './muted'; 9 | 10 | const RootReducer = combineReducers({ 11 | historyDisplay, 12 | history, 13 | displayValue, 14 | calculated, 15 | keyDown, 16 | keys: () => MapKeys, 17 | muted 18 | }); 19 | 20 | export default RootReducer; 21 | -------------------------------------------------------------------------------- /src/scripts/reducers/percent.js: -------------------------------------------------------------------------------- 1 | import { PERCENT } from '../actions/constants'; 2 | import helper from '../model/helper'; 3 | 4 | function percent(state = '0', action) { 5 | const { historyDisplay, displayValue } = action.data; 6 | let output = '0'; 7 | 8 | switch (action.type) { 9 | case PERCENT: 10 | if (!helper.isEmpty(historyDisplay)) { 11 | output = eval(`${helper.commaToPoint(state)}${displayValue}`) / 100; 12 | output = helper.pointToComma(output); 13 | } 14 | 15 | return output; 16 | } 17 | 18 | return state; 19 | } 20 | 21 | export default percent; 22 | -------------------------------------------------------------------------------- /src/scripts/model/helper.js: -------------------------------------------------------------------------------- 1 | 2 | const helper = { 3 | isNumberZero(value) { 4 | return Number(value) === 0; 5 | }, 6 | 7 | hasValue(value) { 8 | return value.length > 0; 9 | }, 10 | 11 | isEmpty(value) { 12 | return value.length === 0; 13 | }, 14 | 15 | commaToPoint(value) { 16 | //in 15,53 17 | //out 15.53 18 | return value.toString().replace(/,/g, '.'); 19 | }, 20 | 21 | pointToComma(value) { 22 | //in 15.53 23 | //out 15,53 24 | return value.toString().replace(/\./g, ','); 25 | }, 26 | 27 | isNaN(value) { 28 | return isNaN(parseFloat(value)); 29 | }, 30 | 31 | isInteger(value) { 32 | return parseInt(value, 10) === Number(value); 33 | }, 34 | 35 | isPositiveNumber(value) { 36 | return Number(helper.commaToPoint(value)) > 0; 37 | }, 38 | 39 | removeLastChar(value) { 40 | return value.toString().substring(0, value.length - 1); 41 | } 42 | }; 43 | 44 | export default helper; 45 | -------------------------------------------------------------------------------- /src/scss/calculator.scss: -------------------------------------------------------------------------------- 1 | @import './variables'; 2 | 3 | .calculator { 4 | display: flex; 5 | flex-direction: column; 6 | height: inherit; 7 | 8 | &__header { 9 | align-content: space-between; 10 | background-color: $air; 11 | display: inherit; 12 | flex-basis: 38%; 13 | flex-wrap: wrap; 14 | padding: 10px; 15 | } 16 | 17 | &__body { 18 | background-color: $base-color; 19 | display: inherit; 20 | flex: 1 auto; 21 | flex-basis: 77%; 22 | flex-wrap: wrap; 23 | } 24 | 25 | &__history { 26 | font-size: 1.1rem; 27 | margin: 0; 28 | opacity: .6; 29 | overflow-x: hidden; 30 | position: relative; 31 | right: -12%; 32 | text-align: right; 33 | width: 89%; 34 | } 35 | 36 | &__result { 37 | font-size: 2.5rem; 38 | margin: 0; 39 | overflow-x: hidden; 40 | padding: 0; 41 | text-align: right; 42 | width: 100%; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "PRODUCTION": true, 4 | "describe": true, 5 | "it": true, 6 | "expect": true, 7 | "beforeEach": true, 8 | "jest": true, 9 | "afterEach": true, 10 | "spyOn": true, 11 | "ga": true 12 | }, 13 | "plugins": [ 14 | "react" 15 | ], 16 | "extends": [ 17 | "eslint:recommended", 18 | "plugin:react/recommended" 19 | ], 20 | "parser": "babel-eslint", 21 | "parserOptions": { 22 | "ecmaVersion": 7, 23 | "sourceType": "module" 24 | }, 25 | "env": { 26 | "es6": true, 27 | "browser": true, 28 | "node": true 29 | }, 30 | "rules": { 31 | "no-const-assign": "warn", 32 | "no-this-before-super": "warn", 33 | "no-undef": "warn", 34 | "no-unreachable": "warn", 35 | "no-unused-vars": "warn", 36 | "constructor-super": "warn", 37 | "valid-typeof": "warn", 38 | "eqeqeq": 1, 39 | "no-console": 0, 40 | "curly": 1, 41 | "quotes": [ 42 | 1, 43 | "single" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /spec/del.spec.js: -------------------------------------------------------------------------------- 1 | import del from '../src/scripts/reducers/del'; 2 | import createAction from '../src/scripts/actions/createAction'; 3 | import * as constants from '../src/scripts/actions/constants'; 4 | import dataFixture from './dataFixture'; 5 | 6 | describe('Del reducer tests', () => { 7 | let data = {}; 8 | 9 | beforeEach(function () { 10 | data = Object.assign({}, data, dataFixture); 11 | }); 12 | 13 | it('deletes char', () => { 14 | const state = '20,12'; 15 | const value = ''; 16 | const action = createAction(constants.DEL, { value, data }); 17 | let result = del(state, action); 18 | 19 | expect(del(state, action)).toBe('20,1'); 20 | 21 | result = del(result, action); 22 | expect(result).toBe('20,'); 23 | 24 | result = del(result, action); 25 | expect(result).toBe('20'); 26 | 27 | result = del(result, action); 28 | expect(result).toBe('2'); 29 | 30 | result = del(result, action); 31 | expect(result).toBe('0'); 32 | 33 | result = del('-2', action); 34 | expect(result).toBe('0'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Calculator](https://calculator.iondrimbafilho.me/images/calctwitter.png) 2 | 3 | ![build](https://github.com/iondrimba/react-calculator/workflows/build/badge.svg?branch=main) 4 | ![deploy](https://github.com/iondrimba/react-calculator/workflows/deploy/badge.svg?branch=main) 5 | [![Test Coverage](https://api.codeclimate.com/v1/badges/a82eed604ee312a0edfa/test_coverage)](https://codeclimate.com/github/iondrimba/react-calculator/test_coverage) 6 | [![Code Climate](https://codeclimate.com/github/iondrimba/react-calculator/badges/gpa.svg)](https://codeclimate.com/github/iondrimba/react-calculator) 7 | 8 | ## [Live demo](https://calculator.iondrimbafilho.me/) 9 | 10 | ## Stack 11 | 12 | - React + Redux 13 | - Sass 14 | - ES6 15 | - Jest + Enzyme 16 | - Webpack 17 | 18 | ## Features 19 | 20 | - PWA 21 | - Offline ready 22 | - Desktop version via manifest (Add to homescreen) 23 | - Responsive 24 | - Keyboard ready 25 | 26 | ## Todo 27 | 28 | - more tests 29 | - animations 30 | 31 | ### Design inspired via [http://collectui.com/designers/pramodrhegde](http://collectui.com/designers/pramodrhegde) 32 | -------------------------------------------------------------------------------- /spec/clear.spec.js: -------------------------------------------------------------------------------- 1 | import clear from '../src/scripts/reducers/clear'; 2 | import createAction from '../src/scripts/actions/createAction'; 3 | import * as constants from '../src/scripts/actions/constants'; 4 | import dataFixture from './dataFixture'; 5 | 6 | describe('Clear reducer tests', () => { 7 | let data = {}; 8 | 9 | beforeEach(function () { 10 | data = Object.assign({}, data, dataFixture); 11 | }); 12 | 13 | it('returns 0 after clear', () => { 14 | const state = '2'; 15 | const value = '99'; 16 | const action = createAction(constants.CLEAR, { value, data }); 17 | const result = clear(state, action); 18 | 19 | expect(result).toBe('0'); 20 | }); 21 | 22 | describe('default action', () => { 23 | describe('when action.type not CLEAR', () => { 24 | it('returns default state', () => { 25 | const state = '2'; 26 | const value = '99'; 27 | const action = createAction(constants.ADD, { value, data }); 28 | const result = clear(state, action); 29 | 30 | expect(result).toBe('2'); 31 | }); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/scripts/components/githubIcon.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import Styles from '../../scss/githubIcon.scss'; 3 | 4 | class GithubIcon extends PureComponent { 5 | render() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | } 13 | 14 | export default GithubIcon; 15 | -------------------------------------------------------------------------------- /src/scripts/reducers/displayValue.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../actions/constants'; 2 | import add from '../reducers/add'; 3 | import clear from '../reducers/clear'; 4 | import del from '../reducers/del'; 5 | import comma from '../reducers/comma'; 6 | import switchOperator from '../reducers/switchOperator'; 7 | import percent from '../reducers/percent'; 8 | import history from '../reducers/history'; 9 | import calc from '../reducers/calc'; 10 | 11 | function displayValue(state = '', action) { 12 | switch (action.type) { 13 | case constants.SWITCH_OPERATOR: 14 | return switchOperator(state, action); 15 | case constants.COMMA: 16 | return comma(state, action); 17 | case constants.DEL: 18 | return del(state, action); 19 | case constants.CALC: 20 | return calc(history(action.data.history, action), action); 21 | case constants.CLEAR: 22 | return clear(state, action); 23 | case constants.PERCENT: 24 | return add(percent(action.data.historyDisplay, action), action); 25 | case constants.ADD: 26 | return comma(add(state, action), action); 27 | } 28 | 29 | return state; 30 | } 31 | 32 | export default displayValue; 33 | -------------------------------------------------------------------------------- /src/scripts/reducers/operator.js: -------------------------------------------------------------------------------- 1 | import { OPERATOR } from '../actions/constants'; 2 | import helper from '../model/helper'; 3 | 4 | function _appendOperatorToZero({ output, formatedValue, value }) { 5 | if (helper.isNaN(formatedValue) || helper.isNumberZero(formatedValue)) { 6 | output = `0 ${value} `; 7 | } 8 | return output; 9 | } 10 | 11 | function _appendOperatorToHistory({ output, historyDisplay, displayValue, value, calculated }) { 12 | if (calculated === false || helper.isEmpty(historyDisplay)) { 13 | output = `${historyDisplay}${displayValue} ${value} `; 14 | } else { 15 | output = historyDisplay.replace(/\D $/, ` ${value} `); 16 | } 17 | return output; 18 | } 19 | 20 | function operator(state = '0', action) { 21 | const { historyDisplay, displayValue, calculated } = action.data; 22 | let output = '0'; 23 | 24 | switch (action.type) { 25 | case OPERATOR: 26 | var formatedValue = helper.commaToPoint(displayValue); 27 | 28 | output = _appendOperatorToZero({ output, formatedValue, value: action.value }) 29 | 30 | output = _appendOperatorToHistory({ output, historyDisplay, displayValue, value: action.value, calculated }) 31 | 32 | return output; 33 | } 34 | 35 | return state; 36 | } 37 | 38 | export default operator; 39 | -------------------------------------------------------------------------------- /spec/comma.spec.js: -------------------------------------------------------------------------------- 1 | import comma from '../src/scripts/reducers/comma'; 2 | import createAction from '../src/scripts/actions/createAction'; 3 | import * as constants from '../src/scripts/actions/constants'; 4 | import dataFixture from './dataFixture'; 5 | 6 | describe('Comma reducer tests', () => { 7 | let data = {}; 8 | 9 | beforeEach(() => { 10 | data = Object.assign({}, data, dataFixture); 11 | }); 12 | 13 | afterEach(() => { 14 | data = Object.assign({}, data, dataFixture); 15 | }); 16 | 17 | it('adds comma to state', () => { 18 | const value = ','; 19 | const action = createAction(constants.COMMA, { value, data }); 20 | 21 | expect(comma('20', action)).toBe('20,'); 22 | expect(comma('0', action)).toBe('0,'); 23 | }); 24 | 25 | it('resets string to 0,', () => { 26 | const state = '150'; 27 | const value = ','; 28 | const action = createAction(constants.COMMA, { value, data }); 29 | 30 | action.data.calculated = true; 31 | 32 | expect(comma(state, action)).toBe('0,'); 33 | }); 34 | 35 | it('does not add another comma', () => { 36 | const state = '150,'; 37 | const value = ','; 38 | const action = createAction(constants.COMMA, { value, data }); 39 | 40 | expect(comma(state, action)).toBe('150,'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /spec/percent.spec.js: -------------------------------------------------------------------------------- 1 | import percent from '../src/scripts/reducers/percent'; 2 | import createAction from '../src/scripts/actions/createAction'; 3 | import * as constants from '../src/scripts/actions/constants'; 4 | import dataFixture from './dataFixture'; 5 | 6 | describe('Clear reducer tests', () => { 7 | let data = {}; 8 | 9 | beforeEach(function () { 10 | data = Object.assign({}, data, dataFixture); 11 | }); 12 | 13 | describe('default action', () => { 14 | describe('when no matching action.type present', () => { 15 | it('returns default state', () => { 16 | const state = '2'; 17 | const value = '99'; 18 | const action = createAction(constants.ADD, { value, data }); 19 | const result = percent(state, action); 20 | 21 | expect(result).toBe('2'); 22 | }); 23 | }); 24 | 25 | describe('when matching action.type present', () => { 26 | it('returns 0,525', () => { 27 | const state = '10,50*'; 28 | const value = '5'; 29 | const action = createAction(constants.PERCENT, { value, data:{ 30 | displayValue: '5', 31 | historyDisplay: '10,50 * ', 32 | calculated: false, 33 | history:[] 34 | }}); 35 | 36 | const result = percent(state, action); 37 | 38 | expect(result).toBe('0,525'); 39 | }); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: build 5 | 6 | on: 7 | push: 8 | branches: [ '*' ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: [15.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - run: npm ci 26 | - run: npm run build 27 | 28 | test: 29 | name: test 30 | runs-on: ubuntu-latest 31 | strategy: 32 | matrix: 33 | node-version: [15.x] 34 | 35 | steps: 36 | - uses: actions/checkout@main 37 | - name: Use Node.js ${{ matrix.node-version }} 38 | uses: actions/setup-node@main 39 | with: 40 | node-version: ${{ matrix.node-version }} 41 | - run: npm ci 42 | - uses: paambaati/codeclimate-action@v2.6.0 43 | env: 44 | CI: true 45 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 46 | with: 47 | coverageCommand: npm test 48 | -------------------------------------------------------------------------------- /src/scripts/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { render } from 'react-dom'; 4 | import { createStore } from 'redux'; 5 | import AppContainer from './container/appContainer.js'; 6 | import RootReducer from './reducers/root'; 7 | import defaultStore from './model/initialState'; 8 | import './../scss/app.scss'; 9 | 10 | let store = createStore(RootReducer, defaultStore, window.devToolsExtension && window.devToolsExtension()); 11 | 12 | render( 13 | 14 | 15 | , 16 | document.getElementById('app') 17 | ); 18 | 19 | if (process.env.NODE_ENV === 'production') { 20 | (function () { 21 | if ('serviceWorker' in navigator) { 22 | window.onload = function () { 23 | navigator.serviceWorker.register('sw.js'); 24 | } 25 | } 26 | })(); 27 | (function (i, s, o, g, r, a, m) { 28 | i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () { 29 | (i[r].q = i[r].q || []).push(arguments) 30 | }, i[r].l = 1 * new Date(); a = s.createElement(o), 31 | m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m) 32 | })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga'); 33 | 34 | ga('create', 'UA-89642554-1', 'auto'); 35 | ga('send', 'pageview'); 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/scripts/components/button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import '../../scss/button.scss'; 4 | 5 | class Button extends React.Component { 6 | constructor() { 7 | super(); 8 | 9 | this.onClick = this.onClick.bind(this); 10 | this.onMouseDown = this.onMouseDown.bind(this); 11 | } 12 | 13 | shouldComponentUpdate(prevProps) { 14 | return prevProps.className !== this.props.className; 15 | } 16 | 17 | isActive() { 18 | const regex = new RegExp(/active/, 'gi'); 19 | 20 | return this.props.className.match(regex) !== null; 21 | } 22 | onClick(evt) { 23 | evt.preventDefault(); 24 | evt.currentTarget.blur(); 25 | 26 | this.props.onClick(this.props.id); 27 | } 28 | onMouseDown(evt) { 29 | evt.preventDefault(); 30 | 31 | this.props.onMouseDown(this.props.id); 32 | } 33 | render() { 34 | return ( 35 | 36 | ); 37 | } 38 | } 39 | 40 | Button.propTypes = { 41 | id: PropTypes.string, 42 | label: PropTypes.string, 43 | onClick: PropTypes.func, 44 | onMouseDown: PropTypes.func, 45 | className: PropTypes.string 46 | }; 47 | 48 | export default Button; 49 | -------------------------------------------------------------------------------- /spec/calculated.spec.js: -------------------------------------------------------------------------------- 1 | import calculated from '../src/scripts/reducers/calculated'; 2 | import createAction from '../src/scripts/actions/createAction'; 3 | import * as constants from '../src/scripts/actions/constants'; 4 | import dataFixture from './dataFixture'; 5 | 6 | describe('Calculated reducer tests', () => { 7 | let data = {}; 8 | 9 | beforeEach(function () { 10 | data = Object.assign({}, data, dataFixture); 11 | }); 12 | 13 | it('calculates flag to true', () => { 14 | const state = false; 15 | const value = true; 16 | const action = createAction(constants.CALCULATED, { value, data }); 17 | const result = calculated(state, action); 18 | 19 | expect(result).toBe(true); 20 | }); 21 | 22 | it('calculates flag to false', () => { 23 | const state = true; 24 | const value = false; 25 | const action = createAction(constants.CALCULATED, { value, data }); 26 | const result = calculated(state, action); 27 | 28 | expect(result).toBe(false); 29 | }); 30 | 31 | describe('default action', () => { 32 | describe('when action.type not CALCULATED', () => { 33 | it('returns default state', () => { 34 | const state = false; 35 | const value = true; 36 | const action = createAction(constants.ADD, { value, data }); 37 | const result = calculated(state, action); 38 | 39 | expect(result).toBe(false); 40 | }); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/scripts/reducers/calc.js: -------------------------------------------------------------------------------- 1 | import { CALC } from '../actions/constants'; 2 | import helper from '../model/helper'; 3 | 4 | function _formatOutput({ output, result }) { 5 | if (helper.isInteger(result)) { 6 | output = result.toString(); 7 | } else { 8 | output = helper.pointToComma(result.toFixed(2)); 9 | } 10 | 11 | return output; 12 | } 13 | 14 | function _calculate({ history, expression }) { 15 | return history.reduce(function (a, b) { 16 | expression = helper.commaToPoint(a); 17 | 18 | return b = eval(eval(expression) + helper.commaToPoint(b)); 19 | }); 20 | } 21 | 22 | function calc(state = [], action) { 23 | const history = [...state]; 24 | const { displayValue } = action.data; 25 | let expression = ''; 26 | let result = 0; 27 | let output = ''; 28 | 29 | switch (action.type) { 30 | case CALC: 31 | if (helper.hasValue(history)) { 32 | if (history.length === 1) { 33 | expression = helper.commaToPoint(history[0]); 34 | result = eval(expression); 35 | } else { 36 | result = _calculate({ history, expression }); 37 | } 38 | 39 | output = _formatOutput({ output, result }); 40 | } else { 41 | output = displayValue; 42 | } 43 | 44 | if (output === 'Infinity' || helper.isNaN(output)) { 45 | output = '0'; 46 | } 47 | 48 | return output; 49 | } 50 | return state; 51 | } 52 | 53 | export default calc; 54 | -------------------------------------------------------------------------------- /src/scss/button.scss: -------------------------------------------------------------------------------- 1 | @import './variables'; 2 | 3 | .button { 4 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 5 | background-color: $button-base-color; 6 | border-bottom: 1px solid $border-color; 7 | border-left: 0; 8 | border-right: 1px solid $border-color; 9 | border-top: 0; 10 | box-shadow: inset -1px -1px 0 0 $button-shadow; 11 | color: $air; 12 | cursor: pointer; 13 | font-size: 1.1rem; 14 | outline: none; 15 | width: 25%; 16 | 17 | &_primaryOperator { 18 | background-color: $primary-color; 19 | border-right: 0; 20 | box-shadow: inset 0 -1px 0 0 rgba(255, 255, 255, .13); 21 | font-size: 1.2rem; 22 | font-weight: 900; 23 | } 24 | 25 | &_runOperator { 26 | background-color: $secondary-color; 27 | border-bottom: 0; 28 | border-right: 0; 29 | box-shadow: 1px 6px 20px 5px $button-shadow; 30 | color: $air; 31 | font-weight: bold; 32 | } 33 | 34 | &_runOperator.active { 35 | background-color: $secondary-color; 36 | box-shadow: inset 1px 5px 9px 0 rgba(0, 0, 0, .37); 37 | } 38 | } 39 | 40 | .active { 41 | box-shadow: inset 0 -3px 8px 0 $button-shadow; 42 | } 43 | 44 | .button:nth-child(17), 45 | .button:nth-child(18), 46 | .button:nth-child(19) { 47 | border-bottom: 0; 48 | box-shadow: inset -1px 0 0 0 $button-shadow; 49 | 50 | &.active { 51 | box-shadow: inset 0 3px 8px 0 $button-shadow; 52 | } 53 | } 54 | 55 | .button:nth-child(19) { 56 | font-size: .8rem; 57 | } 58 | -------------------------------------------------------------------------------- /src/scripts/components/soundIcon.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Styles from '../../scss/soundIcon.scss'; 4 | 5 | class SoundIcon extends Component { 6 | onClick() { 7 | this.props.onClick(!this.props.muted); 8 | } 9 | 10 | shouldComponentUpdate(prevProps) { 11 | return prevProps.muted !== this.props.muted; 12 | } 13 | 14 | getMutedCss(muted) { 15 | let css = Styles.soundIcon; 16 | 17 | if (muted) { 18 | css = `${css} ${Styles['soundIcon--muted']}` 19 | } 20 | 21 | return css; 22 | } 23 | 24 | render() { 25 | return ( 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | ); 35 | } 36 | } 37 | 38 | SoundIcon.propTypes = { 39 | muted: PropTypes.bool, 40 | onClick: PropTypes.func 41 | }; 42 | 43 | export default SoundIcon; 44 | -------------------------------------------------------------------------------- /src/scripts/reducers/history.js: -------------------------------------------------------------------------------- 1 | import { CALC, CLEAR } from '../actions/constants'; 2 | 3 | function _appendValueToNewItem({ state, data }) { 4 | const { historyDisplay, displayValue } = data; 5 | let newItem = ''; 6 | 7 | if (state.length >= 1) { 8 | const operator = historyDisplay.substr(historyDisplay.length - 3, 3); 9 | 10 | newItem = `${operator}${displayValue}`; 11 | } 12 | 13 | return newItem; 14 | } 15 | 16 | function _addToHistory({ data, newItem, state }) { 17 | const { historyDisplay, calculated, displayValue } = data; 18 | let output = []; 19 | 20 | newItem = !calculated ? _appendValueToNewItem({ state, data }) : ''; 21 | newItem = !newItem.length ? `${historyDisplay}${displayValue}` : newItem; 22 | output = !calculated ? [...state, newItem] : []; 23 | 24 | return output; 25 | } 26 | 27 | function _addCalculatedToHistory({ data, state, newItem }) { 28 | let output = [] 29 | const { historyDisplay } = data; 30 | 31 | if (historyDisplay.length) { 32 | output = _addToHistory({ data, newItem, state }); 33 | output = output.length ? output : [...state]; 34 | } 35 | 36 | return output; 37 | } 38 | 39 | function history(state = [], action) { 40 | const newItem = ''; 41 | let output = []; 42 | 43 | switch (action.type) { 44 | case CLEAR: 45 | state = output; 46 | break; 47 | case CALC: 48 | output = _addCalculatedToHistory({data: action.data, state, newItem}); 49 | 50 | return output; 51 | } 52 | return state; 53 | } 54 | 55 | export default history; 56 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: deploy 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node-version: [15.x] 16 | steps: 17 | - uses: actions/checkout@main 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@main 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm ci 23 | - run: npm run build 24 | - name: SSH Deploy 25 | uses: garygrossgarten/github-action-ssh@release 26 | with: 27 | command: cd ${{ secrets.FOLDER }} && git pull origin main 28 | host: ${{ secrets.HOST }} 29 | username: ${{ secrets.USER_NAME }} 30 | privateKey: ${{ secrets.PRIVATE_KEY}} 31 | test: 32 | name: test 33 | runs-on: ubuntu-latest 34 | strategy: 35 | matrix: 36 | node-version: [15.x] 37 | 38 | steps: 39 | - uses: actions/checkout@main 40 | - name: Use Node.js ${{ matrix.node-version }} 41 | uses: actions/setup-node@main 42 | with: 43 | node-version: ${{ matrix.node-version }} 44 | - run: npm ci 45 | - uses: paambaati/codeclimate-action@v2.6.0 46 | env: 47 | CI: true 48 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 49 | with: 50 | coverageCommand: npm test 51 | -------------------------------------------------------------------------------- /public/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Calculator", 3 | "short_name": "Calc", 4 | "theme_color": "#47494b", 5 | "background_color": "#3858c5", 6 | "display": "standalone", 7 | "orientation": "portrait", 8 | "start_url": "/", 9 | "icons": [ 10 | { 11 | "src": "/images/icons/icon-72x72.png", 12 | "sizes": "72x72", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "/images/icons/icon-96x96.png", 17 | "sizes": "96x96", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "/images/icons/icon-128x128.png", 22 | "sizes": "128x128", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "/images/icons/icon-144x144.png", 27 | "sizes": "144x144", 28 | "type": "image/png" 29 | }, 30 | { 31 | "src": "/images/icons/icon-152x152.png", 32 | "sizes": "152x152", 33 | "type": "image/png" 34 | }, 35 | { 36 | "src": "/images/icons/icon-192x192.png", 37 | "sizes": "192x192", 38 | "type": "image/png" 39 | }, 40 | { 41 | "src": "/images/icons/icon-384x384.png", 42 | "sizes": "384x384", 43 | "type": "image/png" 44 | }, 45 | { 46 | "src": "/images/icons/icon-512x512.png", 47 | "sizes": "512x512", 48 | "type": "image/png" 49 | }, 50 | { 51 | "src": "/images/favicons/android-chrome-192x192.png?v=yyaexRKElx", 52 | "sizes": "192x192", 53 | "type": "image/png" 54 | }, 55 | { 56 | "src": "/images/favicons/android-chrome-512x512.png?v=yyaexRKElx", 57 | "sizes": "512x512", 58 | "type": "image/png" 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Calculator", 3 | "short_name": "Calc", 4 | "theme_color": "#47494b", 5 | "background_color": "#3858c5", 6 | "display": "standalone", 7 | "orientation": "portrait", 8 | "start_url": "/", 9 | "icons": [ 10 | { 11 | "src": "/images/icons/icon-72x72.png", 12 | "sizes": "72x72", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "/images/icons/icon-96x96.png", 17 | "sizes": "96x96", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "/images/icons/icon-128x128.png", 22 | "sizes": "128x128", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "/images/icons/icon-144x144.png", 27 | "sizes": "144x144", 28 | "type": "image/png" 29 | }, 30 | { 31 | "src": "/images/icons/icon-152x152.png", 32 | "sizes": "152x152", 33 | "type": "image/png" 34 | }, 35 | { 36 | "src": "/images/icons/icon-192x192.png", 37 | "sizes": "192x192", 38 | "type": "image/png" 39 | }, 40 | { 41 | "src": "/images/icons/icon-384x384.png", 42 | "sizes": "384x384", 43 | "type": "image/png" 44 | }, 45 | { 46 | "src": "/images/icons/icon-512x512.png", 47 | "sizes": "512x512", 48 | "type": "image/png" 49 | }, 50 | { 51 | "src": "/images/favicons/android-chrome-192x192.png?v=yyaexRKElx", 52 | "sizes": "192x192", 53 | "type": "image/png" 54 | }, 55 | { 56 | "src": "/images/favicons/android-chrome-512x512.png?v=yyaexRKElx", 57 | "sizes": "512x512", 58 | "type": "image/png" 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const WebpackCleanupPlugin = require('webpack-cleanup-plugin'); 5 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | const isProduction = (process.env.NODE_ENV === 'production'); 7 | const MinifyPlugin = require('babel-minify-webpack-plugin'); 8 | const common = require('./webpack.common.js'); 9 | 10 | module.exports = merge(common, { 11 | mode: 'production', 12 | output: { 13 | publicPath: '', 14 | }, 15 | plugins: [ 16 | new MiniCssExtractPlugin({ 17 | filename: './css/[name].[hash].css', 18 | }), 19 | new MinifyPlugin(), 20 | new WebpackCleanupPlugin(), 21 | new HtmlWebpackPlugin({ 22 | title: 'Calculator', 23 | minify: { 24 | collapseWhitespace: isProduction, 25 | minifyCSS: isProduction, 26 | minifyJS: isProduction, 27 | removeComments: isProduction 28 | }, 29 | template: './src/index.html', 30 | inject: 'body' 31 | }), 32 | new CopyWebpackPlugin({ 33 | patterns: [ 34 | { 35 | from: 'src/manifest.webmanifest', to: 'manifest.webmanifest' 36 | }, 37 | { 38 | from: 'src/.htaccess' 39 | }, 40 | { 41 | from: 'src/sounds', to: 'sounds' 42 | }, 43 | { 44 | from: 'src/browserconfig.xml', to: 'browserconfig.xml' 45 | }, 46 | { 47 | from: 'src/favicon.ico', to: 'favicon.ico' 48 | }, 49 | { 50 | from: 'src/images', to: 'images' 51 | }, 52 | ] 53 | }), 54 | ] 55 | }); 56 | -------------------------------------------------------------------------------- /src/scss/app-viewport-mobile.scss: -------------------------------------------------------------------------------- 1 | @import './variables'; 2 | 3 | @media screen and (min-width: 450px) { 4 | .home { 5 | width: 98%; 6 | 7 | &__content { 8 | height: 88vh; 9 | margin: 0 auto; 10 | width: inherit; 11 | } 12 | } 13 | } 14 | 15 | @media screen and (min-width: 768px) { 16 | .home { 17 | &__content { 18 | height: 600px; 19 | position: relative; 20 | width: 360px; 21 | } 22 | } 23 | 24 | .github { 25 | display: inline-block; 26 | } 27 | } 28 | 29 | @media screen and (min-width: 1024px) { 30 | .github { 31 | display: inline-block; 32 | } 33 | 34 | .home { 35 | &__content { 36 | height: 600px; 37 | position: relative; 38 | width: 360px; 39 | } 40 | } 41 | 42 | .button { 43 | &:active { 44 | background-color: $button-base-color; 45 | box-shadow: inset 0 -3px 8px 0 $button-shadow; 46 | } 47 | 48 | &:nth-child(17), 49 | &:nth-child(18), 50 | &:nth-child(19) { 51 | &:active { 52 | box-shadow: inset 0 3px 8px 0 $button-shadow; 53 | } 54 | } 55 | 56 | &_primaryOperator:hover { 57 | background-color: darken($primary-color, 5%); 58 | } 59 | 60 | &_runOperator:hover { 61 | background-color: darken($secondary-color, 2%); 62 | } 63 | 64 | &:hover:not(&_runOperator):not(&_primaryOperator):not(:active) { 65 | background-color: lighten($button-base-color, 1%); 66 | } 67 | 68 | &_primaryOperator:active { 69 | background-color: $primary-color; 70 | } 71 | 72 | &_runOperator:active { 73 | background-color: $secondary-color; 74 | box-shadow: inset 1px 5px 9px 0 rgba(0, 0, 0, .37); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/scripts/reducers/add.js: -------------------------------------------------------------------------------- 1 | import { ADD } from '../actions/constants'; 2 | import helper from '../model/helper'; 3 | 4 | function _concatValues(calculated, state, lastCommand, value) { 5 | return calculated || helper.isNumberZero(state) || helper.isNumberZero(state + value) || (helper.isEmpty(lastCommand) && calculated); 6 | } 7 | 8 | function _getLastCommand(commands) { 9 | let lastCommand = []; 10 | 11 | if (commands.length > 1) { 12 | lastCommand = commands.pop(); 13 | } 14 | 15 | return lastCommand; 16 | } 17 | 18 | function _getCommands(historyDisplay) { 19 | let commands = []; 20 | 21 | if (historyDisplay) { 22 | commands = historyDisplay.split(''); 23 | } 24 | 25 | return commands; 26 | } 27 | 28 | function _appendValues({ output, calculated, state, lastCommand, value }) { 29 | if (_concatValues(calculated, state, lastCommand, value)) { 30 | output = value; 31 | } else { 32 | output += `${state}${value}`; 33 | } 34 | 35 | return output; 36 | } 37 | 38 | function _getOutput(conditional, firstResult, secondResult) { 39 | return conditional ? firstResult : secondResult; 40 | } 41 | 42 | const maxDisplay = 15; 43 | 44 | function add(state = '', action) { 45 | let output = ''; 46 | let lastCommand = []; 47 | const { historyDisplay, displayValue, calculated } = action.data; 48 | 49 | switch (action.type) { 50 | case ADD: 51 | lastCommand = _getLastCommand(_getCommands(historyDisplay)); 52 | 53 | output = helper.hasValue(state) ? _appendValues({ output, calculated, state, lastCommand, value: action.value }) : output; 54 | 55 | output = _getOutput(output.length, output, action.value); 56 | 57 | output = _getOutput(output.length > maxDisplay, displayValue, output); 58 | 59 | return output; 60 | } 61 | return state; 62 | } 63 | 64 | export default add; 65 | -------------------------------------------------------------------------------- /src/scripts/views/calculator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Button from '../components/button.js'; 4 | import SoundIcon from '../components/soundIcon'; 5 | import Display from '../components/display'; 6 | import Styles from '../../scss/calculator.scss'; 7 | import StylesButton from '../../scss/button.scss'; 8 | 9 | class Calculator extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | } 13 | render() { 14 | return ( 15 |
16 | 17 |
18 | 19 | 20 |
21 |
{ 22 | this.props.keys.map((elmt, index) => { 23 | var css = this.props.getButtonClass(elmt, StylesButton); 24 | return ( 25 |
31 |
32 | ); 33 | } 34 | } 35 | 36 | Calculator.propTypes = { 37 | muteIconClick: PropTypes.func 38 | , buttonClick: PropTypes.func 39 | , onMouseDown: PropTypes.func 40 | , displayValue: PropTypes.string 41 | , historyDisplay: PropTypes.string 42 | , keyDown: PropTypes.string 43 | , muted: PropTypes.bool 44 | , getButtonClass: PropTypes.func 45 | , isActiveCSS: PropTypes.func 46 | , keys: PropTypes.array 47 | }; 48 | 49 | export default Calculator; 50 | -------------------------------------------------------------------------------- /spec/historyDisplay.spec.js: -------------------------------------------------------------------------------- 1 | import createAction from '../src/scripts/actions/createAction'; 2 | import * as constants from '../src/scripts/actions/constants'; 3 | import dataFixture from './dataFixture'; 4 | import historyDisplay from '../src/scripts/reducers/historyDisplay'; 5 | 6 | describe('HistoryDisplay Reducer tests', () => { 7 | 8 | let data = {}; 9 | 10 | beforeEach(function () { 11 | data = Object.assign({}, data, dataFixture); 12 | }); 13 | 14 | afterEach(function () { 15 | data = Object.assign({}, data, dataFixture); 16 | }); 17 | 18 | it('appends values', () => { 19 | const state = ''; 20 | const value = '+'; 21 | const action = createAction(constants.OPERATOR, { value, data }); 22 | 23 | action.data.displayValue = '59'; 24 | 25 | expect(historyDisplay(state, action)).toBe('59 + '); 26 | }); 27 | 28 | it('matches string', () => { 29 | const state = '59 + '; 30 | const value = '-'; 31 | const action = createAction(constants.OPERATOR, { value, data }); 32 | 33 | action.data.displayValue = '59'; 34 | 35 | expect(historyDisplay(state, action)).toBe('59 - '); 36 | }); 37 | 38 | it('clears historyDisplay', () => { 39 | const state = '20 + 5 * 10'; 40 | const value = '10'; 41 | const action = createAction(constants.CLEAR, { value, data }); 42 | 43 | action.data.historyDisplay = '20 + 5 * 10' 44 | 45 | expect(historyDisplay(state, action)).toBe(''); 46 | }); 47 | 48 | describe('default action', () => { 49 | describe('when no matching action.type present', () => { 50 | it('returns default state', () => { 51 | const state = '59 + '; 52 | const value = '-'; 53 | const action = createAction(constants.ADD, { value, data }); 54 | 55 | action.data.displayValue = '59'; 56 | 57 | expect(historyDisplay(state, action)).toBe('59 + '); 58 | }); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /spec/add.spec.js: -------------------------------------------------------------------------------- 1 | import add from '../src/scripts/reducers/add'; 2 | import createAction from '../src/scripts/actions/createAction'; 3 | import * as constants from '../src/scripts/actions/constants'; 4 | import dataFixture from './dataFixture'; 5 | 6 | describe('Add reducers tests', () => { 7 | let data = {}; 8 | 9 | beforeEach(function () { 10 | data = Object.assign(data, dataFixture); 11 | }); 12 | 13 | it('adds string to display', () => { 14 | const state = ''; 15 | const value = '56'; 16 | const action = createAction(constants.ADD, { value, data }); 17 | const result = add(state, action); 18 | 19 | expect(result).toBe(action.value); 20 | }); 21 | 22 | it('limits display string', () => { 23 | const state = '999999999912345'; 24 | const value = '7'; 25 | 26 | data.displayValue = '999999999912345'; 27 | 28 | const action = createAction(constants.ADD, { value, data }); 29 | const result = add(state, action); 30 | 31 | expect(result.length).toBe(15); 32 | }); 33 | 34 | it('appends string', () => { 35 | const state = '0,'; 36 | const value = '30'; 37 | const action = createAction(constants.ADD, { value, data }); 38 | const result = add(state, action); 39 | 40 | expect(result).toBe('0,30'); 41 | }); 42 | 43 | describe('_getLastCommand', () => { 44 | it('appends string with last command filled', () => { 45 | const localData = Object.assign({}, data, { historyDisplay: '90 +' }); 46 | const state = '0,'; 47 | const value = '30'; 48 | const action = createAction(constants.ADD, { value, data: localData }); 49 | const result = add(state, action); 50 | 51 | expect(result).toBe('0,30'); 52 | }); 53 | }); 54 | 55 | describe('_appendValues', () => { 56 | it('appends string', () => { 57 | const localData = Object.assign({}, data, { historyDisplay: '90 +' }); 58 | const state = ''; 59 | const value = '30'; 60 | const action = createAction(constants.ADD, { value, data: localData }); 61 | const result = add(state, action); 62 | 63 | expect(result).toBe('30'); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/scripts/model/mapKeys.js: -------------------------------------------------------------------------------- 1 | const MapKeys = [ 2 | { 3 | key: 'Delete', 4 | label: 'C', 5 | type: 'command', 6 | command: 'clearAction' 7 | }, 8 | { 9 | key: '+/-', 10 | label: '\u00B1', 11 | type: 'command', 12 | command: 'switchOperatorAction' 13 | }, 14 | { 15 | key: '%', 16 | label: '%', 17 | type: 'percent', 18 | command: 'percentAction' 19 | }, 20 | { 21 | key: '/', 22 | label: '\u00F7', 23 | type: 'operator', 24 | command: 'operatorAction' 25 | }, 26 | { 27 | key: '7', 28 | label: '7', 29 | type: 'number', 30 | command: 'addAction' 31 | }, 32 | { 33 | key: '8', 34 | label: '8', 35 | type: 'number', 36 | command: 'addAction' 37 | }, 38 | { 39 | key: '9', 40 | label: '9', 41 | type: 'number', 42 | command: 'addAction' 43 | }, 44 | { 45 | key: '*', 46 | label: '\u00D7', 47 | type: 'operator', 48 | command: 'operatorAction' 49 | }, 50 | { 51 | key: '4', 52 | label: '4', 53 | type: 'number', 54 | command: 'addAction' 55 | }, 56 | { 57 | key: '5', 58 | label: '5', 59 | type: 'number', 60 | command: 'addAction' 61 | }, 62 | { 63 | key: '6', 64 | label: '6', 65 | type: 'number', 66 | command: 'addAction' 67 | }, 68 | { 69 | key: '-', 70 | label: '\u2212', 71 | type: 'operator', 72 | command: 'operatorAction' 73 | }, 74 | { 75 | key: '1', 76 | label: '1', 77 | type: 'number', 78 | command: 'addAction' 79 | }, 80 | { 81 | key: '2', 82 | label: '2', 83 | type: 'number', 84 | command: 'addAction' 85 | }, 86 | { 87 | key: '3', 88 | label: '3', 89 | type: 'number', 90 | command: 'addAction' 91 | }, 92 | { 93 | key: '+', 94 | label: '+', 95 | type: 'operator', 96 | command: 'operatorAction' 97 | }, 98 | { 99 | key: '0', 100 | label: '0', 101 | type: 'number', 102 | command: 'addAction' 103 | }, 104 | { 105 | key: ',', 106 | label: ',', 107 | type: 'command', 108 | command: 'commaAction' 109 | }, 110 | { 111 | key: 'Backspace', 112 | label: '\u232B', 113 | type: 'command', 114 | command: 'deleteAction' 115 | }, 116 | { 117 | key: 'Enter', 118 | label: '=', 119 | type: 'result', 120 | command: 'resultAction' 121 | } 122 | ]; 123 | 124 | export default MapKeys; 125 | -------------------------------------------------------------------------------- /src/scripts/views/home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Calculator from './calculator'; 4 | import GithubIcon from '../components/githubIcon'; 5 | import Styles from '../../scss/home.scss'; 6 | import Sound from '../model/sound'; 7 | 8 | class Home extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | componentDidMount() { 14 | this.onClick = this.onButtonClick.bind(this); 15 | this.onMouseDown = this.onMouseDown.bind(this); 16 | 17 | document.body.onkeydown = this.onKeyDown.bind(this); 18 | document.body.onkeyup = this.onKeyUp.bind(this); 19 | 20 | document.addEventListener('touchstart', (evt) => { evt.preventDefault() }, { passive: true }); 21 | 22 | this.sound = new Sound(); 23 | this.sound.setup(); 24 | 25 | requestAnimationFrame(() => { 26 | requestAnimationFrame(() => { 27 | document.getElementsByClassName(`${Styles.home}`)[0].classList.add('fadeIn'); 28 | }); 29 | }); 30 | } 31 | 32 | onKeyDown(evt) { 33 | let button = this.calculator.refs[evt.key]; 34 | 35 | if (button && !button.isActive()) { 36 | this.props.keyDownAction(evt.key); 37 | } 38 | } 39 | 40 | onKeyUp(evt) { 41 | let button = this.calculator.refs[evt.key]; 42 | 43 | if (button) { 44 | this.sound.mute(this.props.muted); 45 | this.sound.play(); 46 | } 47 | 48 | this.props.keyDownAction(''); 49 | this.props.keyUpAction(evt.key, this.props); 50 | } 51 | 52 | onButtonClick(key) { 53 | this.sound.mute(this.props.muted); 54 | this.sound.play(); 55 | this.props.keyDownAction(''); 56 | this.props.keyUpAction(key, this.props); 57 | } 58 | 59 | onMouseDown(key) { 60 | this.props.keyDownAction(key); 61 | } 62 | 63 | onMuteIconClick(value) { 64 | this.props.muteAction(value); 65 | } 66 | 67 | render() { 68 | return ( 69 |
70 |
71 | this.calculator = calculator} 72 | {...this.props} 73 | onMouseDown={this.onMouseDown.bind(this)} 74 | buttonClick={this.onButtonClick.bind(this)} 75 | muteIconClick={this.onMuteIconClick.bind(this)} /> 76 |
77 | 78 |
79 | ); 80 | } 81 | } 82 | 83 | Home.propTypes = { 84 | muteAction: PropTypes.func 85 | , muted: PropTypes.bool 86 | , keyDownAction: PropTypes.func 87 | , keyDown: PropTypes.string 88 | , keyUpAction: PropTypes.func 89 | }; 90 | 91 | export default Home; 92 | -------------------------------------------------------------------------------- /spec/history.spec.js: -------------------------------------------------------------------------------- 1 | import createAction from '../src/scripts/actions/createAction'; 2 | import * as constants from '../src/scripts/actions/constants'; 3 | import dataFixture from './dataFixture'; 4 | import history from '../src/scripts/reducers/history'; 5 | 6 | describe('History Reducer tests', () => { 7 | let data = {}; 8 | 9 | beforeEach(() => { 10 | data = Object.assign({}, data, dataFixture); 11 | }); 12 | 13 | afterEach(() => { 14 | data = Object.assign({}, data, dataFixture); 15 | }); 16 | 17 | it('adds item to history', () => { 18 | const state = []; 19 | const value = '+'; 20 | const action = createAction(constants.CALC, { value, data }); 21 | 22 | action.data.displayValue = '59'; 23 | action.data.historyDisplay = '20+'; 24 | 25 | expect(history(state, action).length).toBe(1); 26 | }); 27 | 28 | it('matches historyDisplay', () => { 29 | const state = []; 30 | const value = '+'; 31 | const action = createAction(constants.CALC, { value, data }); 32 | 33 | action.data.displayValue = '59'; 34 | action.data.historyDisplay = '20+'; 35 | 36 | expect(history(state, action)[0]).toBe('20+59'); 37 | }); 38 | 39 | it('has length > 1', () => { 40 | const state = ['20+59']; 41 | const value = '*'; 42 | const action = createAction(constants.CALC, { value, data }); 43 | 44 | action.data.displayValue = '10'; 45 | action.data.historyDisplay = state[0]; 46 | 47 | expect(history(state, action).length).toBe(2); 48 | }); 49 | 50 | it('matches second history item', () => { 51 | const state = ['20 + 59 * ']; 52 | const value = '1'; 53 | const action = createAction(constants.CALC, { value, data }); 54 | 55 | action.data.displayValue = '10'; 56 | action.data.historyDisplay = state[0]; 57 | 58 | expect(history(state, action)[1]).toBe(' * 10'); 59 | }); 60 | 61 | it('does not add item to history', () => { 62 | const state = ['20 + 59', ' * 10']; 63 | const value = '1'; 64 | const action = createAction(constants.CALC, { value, data }); 65 | 66 | action.data.displayValue = '20'; 67 | action.data.calculated = true; 68 | action.data.historyDisplay = '20 + 59 * 10'; 69 | 70 | expect(history(state, action).length).toBe(2); 71 | }); 72 | 73 | it('return empty history', () => { 74 | const state = ['20 + 59', ' * 10']; 75 | const value = '1'; 76 | const action = createAction(constants.CALC, { value, data }); 77 | 78 | action.data.displayValue = '20'; 79 | action.data.calculated = true; 80 | action.data.historyDisplay = ''; 81 | 82 | expect(history(state, action).length).toBe(0); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /spec/calc.spec.js: -------------------------------------------------------------------------------- 1 | import calc from '../src/scripts/reducers/calc'; 2 | import createAction from '../src/scripts/actions/createAction'; 3 | import * as constants from '../src/scripts/actions/constants'; 4 | import dataFixture from './dataFixture'; 5 | 6 | describe('Calc reducers tests', () => { 7 | let data = {}; 8 | 9 | beforeEach(function () { 10 | data = Object.assign({}, data, dataFixture); 11 | }); 12 | 13 | it('calculates add operation', () => { 14 | const state = ['20+30']; 15 | const value = []; 16 | const action = createAction(constants.CALC, { value, data }); 17 | const result = calc(state, action); 18 | 19 | expect(result).toBe('50'); 20 | }); 21 | 22 | it('calculates add decimal operation', () => { 23 | const state = ['20+30', '+0,50']; 24 | const value = []; 25 | const action = createAction(constants.CALC, { value, data }); 26 | const result = calc(state, action); 27 | 28 | expect(result).toBe('50,50'); 29 | }); 30 | 31 | it('calculates add negative operation', () => { 32 | const state = ['-40+30', '+0,50']; 33 | const value = []; 34 | const action = createAction(constants.CALC, { value, data }); 35 | const result = calc(state, action); 36 | 37 | expect(result).toBe('-9,50'); 38 | }); 39 | 40 | it('calculates multiple operation', () => { 41 | const state = ['20+30', '*3']; 42 | const value = []; 43 | const action = createAction(constants.CALC, { value, data }); 44 | const result = calc(state, action); 45 | 46 | expect(result).toBe('150'); 47 | }); 48 | 49 | 50 | it('calculates subtract operation', () => { 51 | const state = ['20+30', '-30']; 52 | const value = []; 53 | const action = createAction(constants.CALC, { value, data }); 54 | const result = calc(state, action); 55 | 56 | expect(result).toBe('20'); 57 | }); 58 | 59 | it('calculates divide operation', () => { 60 | const state = ['20+30', '/2']; 61 | const value = []; 62 | const action = createAction(constants.CALC, { value, data }); 63 | const result = calc(state, action); 64 | expect(result).toBe('25'); 65 | }); 66 | 67 | it('returns 0 if divided by zero', () => { 68 | const state = ['20+30', '/0']; 69 | const value = []; 70 | const action = createAction(constants.CALC, { value, data }); 71 | const result = calc(state, action); 72 | 73 | expect(result).toBe('0'); 74 | }); 75 | 76 | it('returns 0 if zero divided by zero', () => { 77 | const state = ['0', '/0']; 78 | const value = []; 79 | const action = createAction(constants.CALC, { value, data }); 80 | const result = calc(state, action); 81 | 82 | expect(result).toBe('0'); 83 | }); 84 | 85 | it('returns 0 if invalid calc', () => { 86 | const state = ['0,', '/0']; 87 | const value = []; 88 | const action = createAction(constants.CALC, { value, data }); 89 | const result = calc(state, action); 90 | 91 | expect(result).toBe('0'); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /spec/button.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, mount, configure } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import Button from '../src/scripts/components/button.js'; 5 | 6 | configure({ adapter: new Adapter() }); 7 | 8 | describe('Button', () => { 9 | it('renders button', () => { 10 | const wrapper = shallow(