├── __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 | 
2 |
3 | 
4 | 
5 | [](https://codeclimate.com/github/iondrimba/react-calculator/test_coverage)
6 | [](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 | { this.btn = button; }} type={'button'} onTouchStart={this.onMouseDown} onTouchEnd={this.onClick} onMouseDown={this.onMouseDown} onClick={this.onClick} className={this.props.className}>{this.props.label}
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 |
27 | );
28 | })
29 | }
30 |
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( );
11 |
12 | expect(wrapper.props().type).toBe('button');
13 | expect(wrapper.props().children).toBe('Add');
14 | expect(wrapper.props().className).toBe('button');
15 | });
16 |
17 | it('renders active button', () => {
18 | const wrapper = shallow( );
19 |
20 | expect(wrapper.props().className).toBe('button active');
21 | });
22 |
23 | describe('.isActive', () => {
24 | describe('when button has active class', () => {
25 | it('returns true', () => {
26 | const wrapper = mount( );
27 | const instance = wrapper.instance();
28 |
29 | expect(instance.isActive()).toBe(true);
30 | });
31 | });
32 |
33 | describe('when button does not havve active class', () => {
34 | it('returns false', () => {
35 | const wrapper = mount( );
36 | const instance = wrapper.instance();
37 |
38 | expect(instance.isActive()).toBe(false);
39 | });
40 | });
41 | });
42 |
43 | describe('.onClick', () => {
44 | it('calls onClick', () => {
45 | spyOn(Button.prototype, 'onClick');
46 |
47 | const onClick = jest.fn();
48 | const wrapper = mount( );
49 |
50 | wrapper.find('button').simulate('click');
51 |
52 | expect(Button.prototype.onClick).toHaveBeenCalled();
53 | });
54 |
55 | it('calls .props.onClick', () => {
56 | const onClick = jest.fn();
57 | const wrapper = mount( );
58 |
59 | wrapper.find('button').simulate('click');
60 |
61 | expect(onClick).toHaveBeenCalled();
62 | });
63 | });
64 |
65 | describe('.onMouseDown', () => {
66 | it('calls onMouseDown', () => {
67 | spyOn(Button.prototype, 'onMouseDown');
68 |
69 | const onMouseDown = jest.fn();
70 | const wrapper = mount( );
71 |
72 | wrapper.find('button').simulate('mousedown');
73 |
74 | expect(Button.prototype.onMouseDown).toHaveBeenCalled();
75 | });
76 |
77 | it('calls .props.onMouseDown', () => {
78 | const onMouseDown = jest.fn();
79 | const wrapper = mount( );
80 |
81 | wrapper.find('button').simulate('mousedown');
82 |
83 | expect(onMouseDown).toHaveBeenCalled();
84 | });
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/spec/displayValue.spec.js:
--------------------------------------------------------------------------------
1 | import displayValue from '../src/scripts/reducers/displayValue';
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('DisplayValue reducer tests', () => {
7 | let data = {};
8 |
9 | beforeEach(function () {
10 | data = Object.assign({}, data, dataFixture);
11 | });
12 |
13 | it('displays negative value', () => {
14 | const state = '9';
15 | const value = '';
16 | const action = createAction(constants.SWITCH_OPERATOR, { value, data });
17 |
18 | expect(displayValue(state, action)).toBe('-9');
19 | });
20 |
21 | it('displays positive value', () => {
22 | const state = '-9';
23 | const value = '';
24 | const action = createAction(constants.SWITCH_OPERATOR, { value, data });
25 |
26 | expect(displayValue(state, action)).toBe('9');
27 | });
28 |
29 | it('displays value with comma', () => {
30 | const state = '29';
31 | const value = ',';
32 | const action = createAction(constants.COMMA, { value, data });
33 |
34 | expect(displayValue(state, action)).toBe('29,');
35 | });
36 |
37 | it('deletes a char from display value', () => {
38 | const state = '29,56';
39 | const value = '';
40 | const action = createAction(constants.DEL, { value, data });
41 |
42 | expect(displayValue(state, action)).toBe('29,5');
43 | });
44 |
45 | it('clears display value', () => {
46 | const state = '30';
47 | const value = '9';
48 | const action = createAction(constants.CLEAR, { value, data });
49 |
50 | expect(displayValue(state, action)).toBe('0');
51 | });
52 |
53 | it('displays calculated values', () => {
54 | const state = '';
55 | const value = '';
56 | const action = createAction(constants.CALC, { value, data });
57 |
58 | action.data.displayValue = '30';
59 | action.data.historyDisplay = '20+';
60 |
61 | expect(displayValue(state, action)).toBe('50');
62 | });
63 |
64 | it('adds value to display', () => {
65 | expect(displayValue('9', createAction(constants.ADD, { value: '8', data }))).toBe('98');
66 | expect(displayValue('98', createAction(constants.ADD, { value: '4', data }))).toBe('984');
67 | });
68 |
69 | it('displays correct value when doing percent calculation', () => {
70 | const state = '45*';
71 | const value = '';
72 | const action = createAction(constants.PERCENT, { value, data });
73 |
74 | action.data.historyDisplay = '10*'
75 | action.data.displayValue = '45';
76 |
77 | expect(displayValue(state, action)).toBe('4,5');
78 | });
79 |
80 | it('returns zero if no history was added when doing percent calculation', () => {
81 | const state = '45*';
82 | const value = '';
83 | const action = createAction(constants.PERCENT, { value, data });
84 |
85 | action.data.historyDisplay = ''
86 | action.data.displayValue = '45';
87 |
88 | expect(displayValue(state, action)).toBe('0');
89 | });
90 | });
91 |
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin');
2 | const WebpackCleanupPlugin = require('webpack-cleanup-plugin');
3 | const CopyWebpackPlugin = require('copy-webpack-plugin');
4 |
5 | const config = {
6 | resolve: {
7 | extensions: ['.js', '.jsx', '.json', '.mp3', '.ico']
8 | },
9 | entry: {
10 | app: './src/scripts/app'
11 | },
12 | output: {
13 | path: __dirname + '/public',
14 | filename: '[name].[hash].js'
15 | },
16 | module: {
17 | rules: [
18 | {
19 | test: /.js$/,
20 | exclude: /node_modules/,
21 | use: ['babel-loader', 'eslint-loader'],
22 | },
23 | {
24 | test: /\.html$/,
25 | use: ['raw-loader']
26 | },
27 | {
28 | test: /\.css$/,
29 | use: [
30 | { loader: 'style-loader' },
31 | {
32 | loader: 'css-loader',
33 | options: {
34 | modules: {
35 | mode: 'local',
36 | localIdentName: '[local]',
37 | },
38 | importLoaders: true,
39 | sourceMap: true,
40 | localIdentName: '[local]',
41 | }
42 | }
43 | ]
44 | },
45 | {
46 | test: /\.scss$/,
47 | use: [
48 | { loader: 'style-loader' },
49 | {
50 | loader: 'css-loader',
51 | options: {
52 | modules: {
53 | mode: 'local',
54 | localIdentName: '[local]',
55 | },
56 | import: true,
57 | importLoaders: true,
58 | }
59 | },
60 | { loader: 'sass-loader' }
61 | ]
62 | },
63 | {
64 | test: /\.(eot|ttf|woff|woff2)$/,
65 | loader: 'file-loader?name=fonts/[name].[ext]'
66 | },
67 | {
68 | test: /\.(mp3)$/,
69 | loader: 'file-loader?name=sounds/[name].[ext]'
70 | },
71 | {
72 | test: /.*\.(gif|png|jpe?g|svg)$/i,
73 | loaders: [
74 | 'file-loader?hash=sha512&digest=hex&name=images/[name].[hash].[ext]',
75 | 'image-webpack?{optimizationLevel: 7, interlaced: false, pngquant:{quality: "65-90", speed: 4}, mozjpeg: {quality: 65}}'
76 | ]
77 | }
78 | ]
79 | },
80 |
81 | plugins: [
82 | new WebpackCleanupPlugin(),
83 | new HtmlWebpackPlugin({
84 | title: 'Calculator',
85 | template: './src/index.html',
86 | inject: 'body'
87 | }),
88 | new CopyWebpackPlugin({
89 | patterns: [
90 | {
91 | from: 'src/manifest.webmanifest', to: 'manifest.webmanifest'
92 | },
93 | {
94 | from: 'src/.htaccess'
95 | },
96 | {
97 | from: 'src/sounds', to: 'sounds'
98 | },
99 | {
100 | from: 'src/browserconfig.xml', to: 'browserconfig.xml'
101 | },
102 | {
103 | from: 'src/favicon.ico', to: 'favicon.ico'
104 | },
105 | {
106 | from: 'src/images', to: 'images'
107 | },
108 | ]
109 | }),
110 | ]
111 | };
112 |
113 | module.exports = config;
114 |
--------------------------------------------------------------------------------
/public/sw.js:
--------------------------------------------------------------------------------
1 | if(!self.define){const e=e=>{"require"!==e&&(e+=".js");let i=Promise.resolve();return c[e]||(i=new Promise(async i=>{if("document"in self){const c=document.createElement("script");c.src=e,document.head.appendChild(c),c.onload=i}else importScripts(e),i()})),i.then(()=>{if(!c[e])throw new Error(`Module ${e} didn’t register its module`);return c[e]})},i=(i,c)=>{Promise.all(i.map(e)).then(e=>c(1===e.length?e[0]:e))},c={require:Promise.resolve(i)};self.define=(i,a,n)=>{c[i]||(c[i]=Promise.resolve().then(()=>{let c={};const s={uri:location.origin+i.slice(1)};return Promise.all(a.map(i=>{switch(i){case"exports":return c;case"module":return s;default:return e(i)}})).then(e=>{const i=n(...e);return c.default||(c.default=i),c})}))}}define("./sw.js",["./workbox-69b5a3b7"],(function(e){"use strict";self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.precacheAndRoute([{url:"app.e6d5f1b287c1d3689b65.js",revision:"c71043b984c9b551031dffda161acaa4"},{url:"fonts/geosanslight.woff",revision:"03025f1ca4b9a48cdc5d5260244c76d3"},{url:"fonts/geosanslight.woff2",revision:"d90383514a4a5bd3556ad527ee6092b7"},{url:"fonts/rounded_elegance.woff",revision:"1d5230da9ce1c60352068340a5fd4a9b"},{url:"fonts/rounded_elegance.woff2",revision:"05c1672c90045863e6ea0d4a134560b1"},{url:"images/calcgoogleplus.png",revision:"e8cb07e44fd45cc1d690015632956ce0"},{url:"images/calctwitter.png",revision:"8ae967c9aab6e2c9854e0f490d05188c"},{url:"images/favicons/android-chrome-192x192.png",revision:"0d3cf22b0f6e9ebf50b47f82ee3d068d"},{url:"images/favicons/android-chrome-512x512.png",revision:"c59aec5e034ea0eac556a6d98e99c7f9"},{url:"images/favicons/apple-touch-icon-120x120.png",revision:"85c93618b07c81d67b6436c5b92843d4"},{url:"images/favicons/apple-touch-icon-152x152.png",revision:"bee7f9b3020c8e1b6c9be0783e51ba6d"},{url:"images/favicons/apple-touch-icon-180x180.png",revision:"a23f6ea15efc50c232de274d7e1e1137"},{url:"images/favicons/apple-touch-icon-60x60.png",revision:"70782d4f5b86231efe1961c76082e88f"},{url:"images/favicons/apple-touch-icon-76x76.png",revision:"36bab0918b72610fe232e48659700d2b"},{url:"images/favicons/apple-touch-icon.png",revision:"a23f6ea15efc50c232de274d7e1e1137"},{url:"images/favicons/favicon-16x16.png",revision:"8dede73e0b8d12d20a9f56c6c6cafaef"},{url:"images/favicons/favicon-32x32.png",revision:"f1e3f1c3e6010559d46bc18ad6923ac2"},{url:"images/favicons/mstile-144x144.png",revision:"d8fee62fbfc6f25042c76fcad17a2f67"},{url:"images/favicons/mstile-150x150.png",revision:"279dee2dead9329f0ec9fb4bad331a80"},{url:"images/favicons/safari-pinned-tab.svg",revision:"1367476a5723a9ad56297c288bc6b1fb"},{url:"images/icons/icon-128x128.png",revision:"e374ca8977d27eeefc547ca3a4808dda"},{url:"images/icons/icon-144x144.png",revision:"84e52c9e933af4c3a744f809a137e5e2"},{url:"images/icons/icon-152x152.png",revision:"efa68c78d814eeac3ef19bae2260e73a"},{url:"images/icons/icon-192x192.png",revision:"866b474cc2c349cac9162c7722e5bcb1"},{url:"images/icons/icon-384x384.png",revision:"bc187a844d8b7007f84973db047a3a53"},{url:"images/icons/icon-512x512.png",revision:"1f441bca0677a6997f39370e8454c0fd"},{url:"images/icons/icon-72x72.png",revision:"df783b4a9ec0da3ef1df0d9b3a2a44a3"},{url:"images/icons/icon-96x96.png",revision:"87e78fa00b378a6f472d7f9ce280c30c"},{url:"images/speaker.svg",revision:"e22d55cd4edb9fda5acbb140507b4ab1"},{url:"index.html",revision:"b3359f1dfc1adc1b15a4a69108dcc015"}],{})}));
2 | //# sourceMappingURL=sw.js.map
3 |
--------------------------------------------------------------------------------
/src/scripts/container/appContainer.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import Home from '../views/home.js';
3 | import createAction from '../actions/createAction';
4 | import * as constants from '../actions/constants';
5 |
6 | function mapStateToProps(store) {
7 | return store;
8 | }
9 |
10 | function _isActiveCSS(css, key, keyDown, Styles) {
11 | let active = '';
12 | let className = '';
13 |
14 | if (key === keyDown) {
15 | active = Styles.active;
16 | }
17 |
18 | className = `${css} ${active}`;
19 |
20 | return className;
21 | }
22 |
23 | function _getButtonClass(elmt, Styles) {
24 | let css = '';
25 |
26 | if (elmt.type === 'operator') {
27 | css = Styles.button_primaryOperator;
28 | }
29 |
30 | if (elmt.type === 'result') {
31 | css = Styles.button_runOperator;
32 | }
33 |
34 | return `${Styles.button} ${css}`;
35 | }
36 |
37 | function _keyUpAction(key, props) {
38 | const { displayValue, historyDisplay, calculated, history } = props;
39 |
40 | props.keys.map((elmt) => {
41 | if (key === elmt.key) {
42 | props[elmt.command](key, { displayValue, historyDisplay, calculated, history });
43 | }
44 | });
45 |
46 | return false;
47 | }
48 |
49 | function _dispatchAction(dispatch, action, value) {
50 | dispatch(createAction(action, value));
51 | }
52 |
53 | function _resultAction(dispatch, value, data) {
54 | _dispatchAction(dispatch, constants.CALC, { value, data });
55 | _dispatchAction(dispatch, constants.CALCULATED, { value: true });
56 | _dispatchAction(dispatch, constants.HISTORY_CLEAR, { value, data });
57 | }
58 |
59 | function _commonActions({ constants, dispatch, value, data }) {
60 | const [first, second ] = constants;
61 | _dispatchAction(dispatch, first, { value, data });
62 | _dispatchAction(dispatch, second, { value: false });
63 | }
64 |
65 | function _operatorAction(dispatch, value, data) {
66 | _dispatchAction(dispatch, constants.OPERATOR, { value, data });
67 | _dispatchAction(dispatch, constants.CALC, { value, data });
68 | _dispatchAction(dispatch, constants.CALCULATED, { value: true });
69 | }
70 |
71 | const mapDispatchToProps = (dispatch) => {
72 | return {
73 | keyUpAction: (key, props) => { _keyUpAction(key, props); },
74 | muteAction: (value) => { _dispatchAction(dispatch, constants.MUTED, { value }); },
75 | keyDownAction: (value) => { _dispatchAction(dispatch, constants.KEY_DOWN, { value }); },
76 | resultAction: (value, data) => { _resultAction(dispatch, value, data); },
77 | clearAction: (value, data) => { _dispatchAction(dispatch, constants.CLEAR, { value, data }); },
78 | deleteAction: (value, data) => { _dispatchAction(dispatch, constants.DEL, { value, data }); },
79 | operatorAction: (value, data) => { _operatorAction(dispatch, value, data); },
80 | addAction: (value, data) => { _commonActions({ constants: [constants.ADD, constants.CALCULATED], dispatch, value, data }); },
81 | commaAction: (value, data) => { _commonActions({ constants: [constants.COMMA, constants.CALCULATED], dispatch, value, data }); },
82 | switchOperatorAction: (value, data) => { _commonActions({ constants: [constants.SWITCH_OPERATOR, constants.CALCULATED], dispatch, value, data }); },
83 | percentAction: (value, data) => { _commonActions({ constants: [constants.PERCENT, constants.CALCULATED], dispatch, value, data }) },
84 | isActiveCSS: (css, key, keyDown, Styles) => { return _isActiveCSS(css, key, keyDown, Styles); },
85 | getButtonClass: (elmt, Styles) => { return _getButtonClass(elmt, Styles); }
86 | };
87 | }
88 |
89 | const AppContainer = connect(mapStateToProps, mapDispatchToProps)(Home);
90 |
91 | export default AppContainer;
92 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "calc",
3 | "version": "1.1.0",
4 | "main": "",
5 | "author": "Ion D. FIlho ",
6 | "license": "MIT",
7 | "scripts": {
8 | "start": "cross-env NODE_ENV=development ./node_modules/.bin/webpack-dev-server --open --config webpack.dev.js",
9 | "prod": "cross-env NODE_ENV=production ./node_modules/.bin/webpack -p --config webpack.prod.js",
10 | "test": "./node_modules/.bin/jest --coverage --testMatch '**/*.spec.js'",
11 | "workbox": "./node_modules/.bin/workbox generateSW workbox-config.js",
12 | "build": "npm run prod && npm run workbox"
13 | },
14 | "engines": {
15 | "node": "15.11.0",
16 | "npm": "7.6.0"
17 | },
18 | "dependencies": {
19 | "@babel/polyfill": "^7.12.1",
20 | "@babel/runtime": "^7.13.17",
21 | "prop-types": "^15.7.2"
22 | },
23 | "jest": {
24 | "transform": {
25 | "^.+\\.js?$": "babel-jest"
26 | },
27 | "moduleNameMapper": {
28 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js",
29 | "\\.(css|scss)$": "/__mocks__/styleMock.js"
30 | },
31 | "notify": false,
32 | "clearMocks": true,
33 | "restoreMocks": true,
34 | "collectCoverage": true,
35 | "collectCoverageFrom": [
36 | "src/**/*.{js,jsx,ts}"
37 | ]
38 | },
39 | "devDependencies": {
40 | "@babel/cli": "^7.13.16",
41 | "@babel/core": "^7.13.16",
42 | "@babel/helper-compilation-targets": "^7.13.10",
43 | "@babel/plugin-proposal-object-rest-spread": "^7.13.8",
44 | "@babel/plugin-transform-runtime": "^7.13.15",
45 | "@babel/preset-env": "^7.14.0",
46 | "@babel/preset-react": "^7.13.13",
47 | "@babel/register": "^7.13.16",
48 | "autoprefixer": "10.2.5",
49 | "babel-core": "^7.0.0-bridge.0",
50 | "babel-eslint": "^10.1.0",
51 | "babel-jest": "^26.6.3",
52 | "babel-loader": "^8.2.2",
53 | "babel-minify-webpack-plugin": "^0.3.1",
54 | "babel-plugin-webpack-loaders": "0.9.0",
55 | "braces": "^3.0.2",
56 | "compression-webpack-plugin": "6.1.1",
57 | "copy-webpack-plugin": "6.4.1",
58 | "cross-env": "7.0.3",
59 | "css-loader": "5.2.4",
60 | "enzyme": "^3.11.0",
61 | "enzyme-adapter-react-16": "^1.15.6",
62 | "eslint": "7.26.0",
63 | "eslint-loader": "4.0.2",
64 | "eslint-plugin-react": "7.23.2",
65 | "extract-text-webpack-plugin": "3.0.2",
66 | "file-loader": "6.2.0",
67 | "howler": "2.2.1",
68 | "html-loader": "1.3.2",
69 | "html-webpack-plugin": "4.5.2",
70 | "image-webpack-loader": "^7.0.1",
71 | "jest": "^26.6.3",
72 | "json-loader": "0.5.7",
73 | "mini-css-extract-plugin": "^1.5.1",
74 | "node-sass": "^5.0.0",
75 | "postcss-cssnext": "3.1.0",
76 | "postcss-loader": "4.2.0",
77 | "raw-loader": "4.0.2",
78 | "react": "16.14.0",
79 | "react-addons-test-utils": "15.6.2",
80 | "react-dom": "16.14.0",
81 | "react-redux": "7.2.4",
82 | "react-test-renderer": "16.14.0",
83 | "redux": "4.0.5",
84 | "resolve-url-loader": "3.1.3",
85 | "sass-loader": "10.1.1",
86 | "style-loader": "2.0.0",
87 | "stylefmt": "^6.0.3",
88 | "stylelint-config-standard": "22.0.0",
89 | "stylelint-webpack-plugin": "^2.1.1",
90 | "sw-precache-webpack-plugin": "1.0.0",
91 | "url-loader": "^4.1.1",
92 | "webpack": "4.46.0",
93 | "webpack-cleanup-plugin": "0.5.1",
94 | "webpack-cli": "^3.3.12",
95 | "webpack-dev-server": "3.11.3",
96 | "webpack-merge": "^5.7.3",
97 | "workbox-cli": "^6.1.5",
98 | "workbox-webpack-plugin": "^6.1.5"
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/spec/helper.spec.js:
--------------------------------------------------------------------------------
1 | import helper from '../src/scripts/model/helper';
2 |
3 | describe('Helper tests', () => {
4 | it('isNumberZero - should return true', () => {
5 | let result = helper.isNumberZero('0');
6 | expect(result).toBe(true);
7 |
8 | result = helper.isNumberZero(0);
9 | expect(result).toBe(true);
10 | });
11 |
12 | it('isNumberZero - should return false', () => {
13 | let result = helper.isNumberZero('1');
14 | expect(result).toBe(false);
15 |
16 | result = helper.isNumberZero(1);
17 | expect(result).toBe(false);
18 | });
19 |
20 | it('hasValue - should return true', () => {
21 | let result = helper.hasValue('1');
22 | expect(result).toBe(true);
23 |
24 | result = helper.hasValue([1]);
25 | expect(result).toBe(true);
26 | });
27 |
28 | it('hasValue - should return false', () => {
29 | let result = helper.hasValue('');
30 | expect(result).toBe(false);
31 |
32 | result = helper.hasValue([]);
33 | expect(result).toBe(false);
34 | });
35 |
36 | it('isEmpty - should return true', () => {
37 | let result = helper.isEmpty('');
38 | expect(result).toBe(true);
39 |
40 | result = helper.isEmpty([]);
41 | expect(result).toBe(true);
42 | });
43 |
44 | it('isEmpty - should return false', () => {
45 | let result = helper.isEmpty('1');
46 | expect(result).toBe(false);
47 |
48 | result = helper.isEmpty([1]);
49 | expect(result).toBe(false);
50 | });
51 |
52 | it('commaToPoint - should convert string 15,23 to 15.23 ', () => {
53 | let result = helper.commaToPoint('15,23');
54 | expect(result).toBe('15.23');
55 | });
56 |
57 | it('pointToComma - should convert string 15.23 to 15,23 ', () => {
58 | let result = helper.pointToComma('15.23');
59 | expect(result).toBe('15,23');
60 | });
61 |
62 | it('isNaN - should return true for abc ', () => {
63 | let result = helper.isNaN('abc');
64 | expect(result).toBe(true);
65 | });
66 |
67 | it('isNaN - should return false', () => {
68 | let result = helper.isNaN('-15,23');
69 | expect(result).toBe(false);
70 |
71 | result = helper.isNaN('15.23');
72 | expect(result).toBe(false);
73 |
74 | result = helper.isNaN('-0.23');
75 | expect(result).toBe(false);
76 |
77 | result = helper.isNaN(.5);
78 | expect(result).toBe(false);
79 |
80 | result = helper.isNaN(-5.5);
81 | expect(result).toBe(false);
82 | });
83 |
84 | it('isInteger - should return true', () => {
85 | let result = helper.isInteger(1);
86 | expect(result).toBe(true);
87 |
88 | result = helper.isInteger(-1);
89 | expect(result).toBe(true);
90 |
91 | result = helper.isInteger('-1');
92 | expect(result).toBe(true);
93 |
94 | result = helper.isInteger('1');
95 | expect(result).toBe(true);
96 | });
97 |
98 | it('isInteger - should return false', () => {
99 | let result = helper.isInteger(1.5);
100 | expect(result).toBe(false);
101 |
102 | result = helper.isInteger(-1.5);
103 | expect(result).toBe(false);
104 |
105 | result = helper.isInteger('-1.5');
106 | expect(result).toBe(false);
107 | });
108 |
109 | it('isPositiveNumber - should return true', () => {
110 | let result = helper.isPositiveNumber(1.5);
111 | expect(result).toBe(true);
112 |
113 | result = helper.isPositiveNumber(1);
114 | expect(result).toBe(true);
115 |
116 | result = helper.isPositiveNumber(.5);
117 | expect(result).toBe(true);
118 |
119 | result = helper.isPositiveNumber('.5');
120 | expect(result).toBe(true);
121 | });
122 |
123 | it('isPositiveNumber - should return false', () => {
124 | let result = helper.isPositiveNumber(-1.5);
125 | expect(result).toBe(false);
126 |
127 | result = helper.isPositiveNumber('-1');
128 | expect(result).toBe(false);
129 |
130 | result = helper.isPositiveNumber(-.5);
131 | expect(result).toBe(false);
132 |
133 | result = helper.isPositiveNumber('-.5');
134 | expect(result).toBe(false);
135 | });
136 |
137 | it('removeLastChar - should return 123,5', () => {
138 | let result = helper.removeLastChar('123,58');
139 | expect(result).toBe('123,5');
140 | });
141 | });
142 |
--------------------------------------------------------------------------------
/public/sw.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"sw.js","sources":["../../../tmp/e68cabf599f94b6414512eb0f5997a84/sw.js"],"sourcesContent":["import {precacheAndRoute as workbox_precaching_precacheAndRoute} from '/home/ion/react-calculator/node_modules/workbox-precaching/precacheAndRoute.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\n\n\n\n\n\n\nself.addEventListener('message', (event) => {\n if (event.data && event.data.type === 'SKIP_WAITING') {\n self.skipWaiting();\n }\n});\n\n\n\n\n/**\n * The precacheAndRoute() method efficiently caches and responds to\n * requests for URLs in the manifest.\n * See https://goo.gl/S9QRab\n */\nworkbox_precaching_precacheAndRoute([\n {\n \"url\": \"app.e6d5f1b287c1d3689b65.js\",\n \"revision\": \"c71043b984c9b551031dffda161acaa4\"\n },\n {\n \"url\": \"fonts/geosanslight.woff\",\n \"revision\": \"03025f1ca4b9a48cdc5d5260244c76d3\"\n },\n {\n \"url\": \"fonts/geosanslight.woff2\",\n \"revision\": \"d90383514a4a5bd3556ad527ee6092b7\"\n },\n {\n \"url\": \"fonts/rounded_elegance.woff\",\n \"revision\": \"1d5230da9ce1c60352068340a5fd4a9b\"\n },\n {\n \"url\": \"fonts/rounded_elegance.woff2\",\n \"revision\": \"05c1672c90045863e6ea0d4a134560b1\"\n },\n {\n \"url\": \"images/calcgoogleplus.png\",\n \"revision\": \"e8cb07e44fd45cc1d690015632956ce0\"\n },\n {\n \"url\": \"images/calctwitter.png\",\n \"revision\": \"8ae967c9aab6e2c9854e0f490d05188c\"\n },\n {\n \"url\": \"images/favicons/android-chrome-192x192.png\",\n \"revision\": \"0d3cf22b0f6e9ebf50b47f82ee3d068d\"\n },\n {\n \"url\": \"images/favicons/android-chrome-512x512.png\",\n \"revision\": \"c59aec5e034ea0eac556a6d98e99c7f9\"\n },\n {\n \"url\": \"images/favicons/apple-touch-icon-120x120.png\",\n \"revision\": \"85c93618b07c81d67b6436c5b92843d4\"\n },\n {\n \"url\": \"images/favicons/apple-touch-icon-152x152.png\",\n \"revision\": \"bee7f9b3020c8e1b6c9be0783e51ba6d\"\n },\n {\n \"url\": \"images/favicons/apple-touch-icon-180x180.png\",\n \"revision\": \"a23f6ea15efc50c232de274d7e1e1137\"\n },\n {\n \"url\": \"images/favicons/apple-touch-icon-60x60.png\",\n \"revision\": \"70782d4f5b86231efe1961c76082e88f\"\n },\n {\n \"url\": \"images/favicons/apple-touch-icon-76x76.png\",\n \"revision\": \"36bab0918b72610fe232e48659700d2b\"\n },\n {\n \"url\": \"images/favicons/apple-touch-icon.png\",\n \"revision\": \"a23f6ea15efc50c232de274d7e1e1137\"\n },\n {\n \"url\": \"images/favicons/favicon-16x16.png\",\n \"revision\": \"8dede73e0b8d12d20a9f56c6c6cafaef\"\n },\n {\n \"url\": \"images/favicons/favicon-32x32.png\",\n \"revision\": \"f1e3f1c3e6010559d46bc18ad6923ac2\"\n },\n {\n \"url\": \"images/favicons/mstile-144x144.png\",\n \"revision\": \"d8fee62fbfc6f25042c76fcad17a2f67\"\n },\n {\n \"url\": \"images/favicons/mstile-150x150.png\",\n \"revision\": \"279dee2dead9329f0ec9fb4bad331a80\"\n },\n {\n \"url\": \"images/favicons/safari-pinned-tab.svg\",\n \"revision\": \"1367476a5723a9ad56297c288bc6b1fb\"\n },\n {\n \"url\": \"images/icons/icon-128x128.png\",\n \"revision\": \"e374ca8977d27eeefc547ca3a4808dda\"\n },\n {\n \"url\": \"images/icons/icon-144x144.png\",\n \"revision\": \"84e52c9e933af4c3a744f809a137e5e2\"\n },\n {\n \"url\": \"images/icons/icon-152x152.png\",\n \"revision\": \"efa68c78d814eeac3ef19bae2260e73a\"\n },\n {\n \"url\": \"images/icons/icon-192x192.png\",\n \"revision\": \"866b474cc2c349cac9162c7722e5bcb1\"\n },\n {\n \"url\": \"images/icons/icon-384x384.png\",\n \"revision\": \"bc187a844d8b7007f84973db047a3a53\"\n },\n {\n \"url\": \"images/icons/icon-512x512.png\",\n \"revision\": \"1f441bca0677a6997f39370e8454c0fd\"\n },\n {\n \"url\": \"images/icons/icon-72x72.png\",\n \"revision\": \"df783b4a9ec0da3ef1df0d9b3a2a44a3\"\n },\n {\n \"url\": \"images/icons/icon-96x96.png\",\n \"revision\": \"87e78fa00b378a6f472d7f9ce280c30c\"\n },\n {\n \"url\": \"images/speaker.svg\",\n \"revision\": \"e22d55cd4edb9fda5acbb140507b4ab1\"\n },\n {\n \"url\": \"index.html\",\n \"revision\": \"b3359f1dfc1adc1b15a4a69108dcc015\"\n }\n], {});\n\n\n\n\n\n\n\n\n"],"names":["self","addEventListener","event","data","type","skipWaiting"],"mappings":"8xBAmBAA,KAAKC,iBAAiB,UAAYC,IAC5BA,EAAMC,MAA4B,iBAApBD,EAAMC,KAAKC,MAC3BJ,KAAKK,mCAY2B,CAClC,KACS,uCACK,oCAEd,KACS,mCACK,oCAEd,KACS,oCACK,oCAEd,KACS,uCACK,oCAEd,KACS,wCACK,oCAEd,KACS,qCACK,oCAEd,KACS,kCACK,oCAEd,KACS,sDACK,oCAEd,KACS,sDACK,oCAEd,KACS,wDACK,oCAEd,KACS,wDACK,oCAEd,KACS,wDACK,oCAEd,KACS,sDACK,oCAEd,KACS,sDACK,oCAEd,KACS,gDACK,oCAEd,KACS,6CACK,oCAEd,KACS,6CACK,oCAEd,KACS,8CACK,oCAEd,KACS,8CACK,oCAEd,KACS,iDACK,oCAEd,KACS,yCACK,oCAEd,KACS,yCACK,oCAEd,KACS,yCACK,oCAEd,KACS,yCACK,oCAEd,KACS,yCACK,oCAEd,KACS,yCACK,oCAEd,KACS,uCACK,oCAEd,KACS,uCACK,oCAEd,KACS,8BACK,oCAEd,KACS,sBACK,qCAEb"}
--------------------------------------------------------------------------------
/public/workbox-69b5a3b7.js:
--------------------------------------------------------------------------------
1 | define("./workbox-69b5a3b7.js",["exports"],(function(e){"use strict";try{self["workbox:core:5.1.4"]&&_()}catch(e){}const t={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},n=e=>[t.prefix,e,t.suffix].filter(e=>e&&e.length>0).join("-"),s=e=>e||n(t.precache),i=e=>new URL(String(e),location.href).href.replace(new RegExp("^"+location.origin),""),c=(e,...t)=>{let n=e;return t.length>0&&(n+=" :: "+JSON.stringify(t)),n};class o extends Error{constructor(e,t){super(c(e,t)),this.name=e,this.details=t}}const r=new Set;const a=(e,t)=>e.filter(e=>t in e),u=async({request:e,mode:t,plugins:n=[]})=>{const s=a(n,"cacheKeyWillBeUsed");let i=e;for(const e of s)i=await e.cacheKeyWillBeUsed.call(e,{mode:t,request:i}),"string"==typeof i&&(i=new Request(i));return i},l=async({cacheName:e,request:t,event:n,matchOptions:s,plugins:i=[]})=>{const c=await self.caches.open(e),o=await u({plugins:i,request:t,mode:"read"});let r=await c.match(o,s);for(const t of i)if("cachedResponseWillBeUsed"in t){const i=t.cachedResponseWillBeUsed;r=await i.call(t,{cacheName:e,event:n,matchOptions:s,cachedResponse:r,request:o})}return r},h=async({cacheName:e,request:t,response:n,event:s,plugins:c=[],matchOptions:h})=>{const f=await u({plugins:c,request:t,mode:"write"});if(!n)throw new o("cache-put-with-no-response",{url:i(f.url)});const w=await(async({request:e,response:t,event:n,plugins:s=[]})=>{let i=t,c=!1;for(const t of s)if("cacheWillUpdate"in t){c=!0;const s=t.cacheWillUpdate;if(i=await s.call(t,{request:e,response:i,event:n}),!i)break}return c||(i=i&&200===i.status?i:void 0),i||null})({event:s,plugins:c,response:n,request:f});if(!w)return;const d=await self.caches.open(e),p=a(c,"cacheDidUpdate"),y=p.length>0?await l({cacheName:e,matchOptions:h,request:f}):null;try{await d.put(f,w)}catch(e){throw"QuotaExceededError"===e.name&&await async function(){for(const e of r)await e()}(),e}for(const t of p)await t.cacheDidUpdate.call(t,{cacheName:e,event:s,oldResponse:y,newResponse:w,request:f})},f=async({request:e,fetchOptions:t,event:n,plugins:s=[]})=>{if("string"==typeof e&&(e=new Request(e)),n instanceof FetchEvent&&n.preloadResponse){const e=await n.preloadResponse;if(e)return e}const i=a(s,"fetchDidFail"),c=i.length>0?e.clone():null;try{for(const t of s)if("requestWillFetch"in t){const s=t.requestWillFetch,i=e.clone();e=await s.call(t,{request:i,event:n})}}catch(e){throw new o("plugin-error-request-will-fetch",{thrownError:e})}const r=e.clone();try{let i;i="navigate"===e.mode?await fetch(e):await fetch(e,t);for(const e of s)"fetchDidSucceed"in e&&(i=await e.fetchDidSucceed.call(e,{event:n,request:r,response:i}));return i}catch(e){for(const t of i)await t.fetchDidFail.call(t,{error:e,event:n,originalRequest:c.clone(),request:r.clone()});throw e}};let w;async function d(e,t){const n=e.clone(),s={headers:new Headers(n.headers),status:n.status,statusText:n.statusText},i=t?t(s):s,c=function(){if(void 0===w){const e=new Response("");if("body"in e)try{new Response(e.body),w=!0}catch(e){w=!1}w=!1}return w}()?n.body:await n.blob();return new Response(c,i)}try{self["workbox:precaching:5.1.4"]&&_()}catch(e){}function p(e){if(!e)throw new o("add-to-cache-list-unexpected-type",{entry:e});if("string"==typeof e){const t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}const{revision:t,url:n}=e;if(!n)throw new o("add-to-cache-list-unexpected-type",{entry:e});if(!t){const e=new URL(n,location.href);return{cacheKey:e.href,url:e.href}}const s=new URL(n,location.href),i=new URL(n,location.href);return s.searchParams.set("__WB_REVISION__",t),{cacheKey:s.href,url:i.href}}class y{constructor(e){this.t=s(e),this.s=new Map,this.i=new Map,this.o=new Map}addToCacheList(e){const t=[];for(const n of e){"string"==typeof n?t.push(n):n&&void 0===n.revision&&t.push(n.url);const{cacheKey:e,url:s}=p(n),i="string"!=typeof n&&n.revision?"reload":"default";if(this.s.has(s)&&this.s.get(s)!==e)throw new o("add-to-cache-list-conflicting-entries",{firstEntry:this.s.get(s),secondEntry:e});if("string"!=typeof n&&n.integrity){if(this.o.has(e)&&this.o.get(e)!==n.integrity)throw new o("add-to-cache-list-conflicting-integrities",{url:s});this.o.set(e,n.integrity)}if(this.s.set(s,e),this.i.set(s,i),t.length>0){const e=`Workbox is precaching URLs without revision info: ${t.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(e)}}}async install({event:e,plugins:t}={}){const n=[],s=[],i=await self.caches.open(this.t),c=await i.keys(),o=new Set(c.map(e=>e.url));for(const[e,t]of this.s)o.has(t)?s.push(e):n.push({cacheKey:t,url:e});const r=n.map(({cacheKey:n,url:s})=>{const i=this.o.get(n),c=this.i.get(s);return this.u({cacheKey:n,cacheMode:c,event:e,integrity:i,plugins:t,url:s})});await Promise.all(r);return{updatedURLs:n.map(e=>e.url),notUpdatedURLs:s}}async activate(){const e=await self.caches.open(this.t),t=await e.keys(),n=new Set(this.s.values()),s=[];for(const i of t)n.has(i.url)||(await e.delete(i),s.push(i.url));return{deletedURLs:s}}async u({cacheKey:e,url:t,cacheMode:n,event:s,plugins:i,integrity:c}){const r=new Request(t,{integrity:c,cache:n,credentials:"same-origin"});let a,u=await f({event:s,plugins:i,request:r});for(const e of i||[])"cacheWillUpdate"in e&&(a=e);if(!(a?await a.cacheWillUpdate({event:s,request:r,response:u}):u.status<400))throw new o("bad-precaching-response",{url:t,status:u.status});u.redirected&&(u=await d(u)),await h({event:s,plugins:i,response:u,request:e===t?r:new Request(e),cacheName:this.t,matchOptions:{ignoreSearch:!0}})}getURLsToCacheKeys(){return this.s}getCachedURLs(){return[...this.s.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this.s.get(t.href)}async matchPrecache(e){const t=e instanceof Request?e.url:e,n=this.getCacheKeyForURL(t);if(n){return(await self.caches.open(this.t)).match(n)}}createHandler(e=!0){return async({request:t})=>{try{const e=await this.matchPrecache(t);if(e)return e;throw new o("missing-precache-entry",{cacheName:this.t,url:t instanceof Request?t.url:t})}catch(n){if(e)return fetch(t);throw n}}}createHandlerBoundToURL(e,t=!0){if(!this.getCacheKeyForURL(e))throw new o("non-precached-url",{url:e});const n=this.createHandler(t),s=new Request(e);return()=>n({request:s})}}let g;const R=()=>(g||(g=new y),g);const q=(e,t)=>{const n=R().getURLsToCacheKeys();for(const s of function*(e,{ignoreURLParametersMatching:t,directoryIndex:n,cleanURLs:s,urlManipulation:i}={}){const c=new URL(e,location.href);c.hash="",yield c.href;const o=function(e,t=[]){for(const n of[...e.searchParams.keys()])t.some(e=>e.test(n))&&e.searchParams.delete(n);return e}(c,t);if(yield o.href,n&&o.pathname.endsWith("/")){const e=new URL(o.href);e.pathname+=n,yield e.href}if(s){const e=new URL(o.href);e.pathname+=".html",yield e.href}if(i){const e=i({url:c});for(const t of e)yield t.href}}(e,t)){const e=n.get(s);if(e)return e}};let U=!1;function m(e){U||((({ignoreURLParametersMatching:e=[/^utm_/],directoryIndex:t="index.html",cleanURLs:n=!0,urlManipulation:i}={})=>{const c=s();self.addEventListener("fetch",s=>{const o=q(s.request.url,{cleanURLs:n,directoryIndex:t,ignoreURLParametersMatching:e,urlManipulation:i});if(!o)return;let r=self.caches.open(c).then(e=>e.match(o)).then(e=>e||fetch(o));s.respondWith(r)})})(e),U=!0)}const v=[],L={get:()=>v,add(e){v.push(...e)}},x=e=>{const t=R(),n=L.get();e.waitUntil(t.install({event:e,plugins:n}).catch(e=>{throw e}))},K=e=>{const t=R();e.waitUntil(t.activate())};e.precacheAndRoute=function(e,t){!function(e){R().addToCacheList(e),e.length>0&&(self.addEventListener("install",x),self.addEventListener("activate",K))}(e),m(t)}}));
2 | //# sourceMappingURL=workbox-69b5a3b7.js.map
3 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 | PWA React Calculator almost there, hang on...
--------------------------------------------------------------------------------
/src/GzipSimpleHTTPServer.py:
--------------------------------------------------------------------------------
1 | """Simple HTTP Server.
2 |
3 | This module builds on BaseHTTPServer by implementing the standard GET
4 | and HEAD requests in a fairly straightforward manner.
5 |
6 | """
7 |
8 |
9 | __version__ = "0.6"
10 |
11 | __all__ = ["SimpleHTTPRequestHandler"]
12 |
13 | import os
14 | import posixpath
15 | import BaseHTTPServer
16 | import urllib
17 | import cgi
18 | import sys
19 | import mimetypes
20 | import zlib
21 | from optparse import OptionParser
22 |
23 | try:
24 | from cStringIO import StringIO
25 | except ImportError:
26 | from StringIO import StringIO
27 |
28 | SERVER_PORT = 8000
29 | encoding_type = 'gzip'
30 |
31 | def parse_options():
32 | # Option parsing logic.
33 | parser = OptionParser()
34 | parser.add_option("-e", "--encoding", dest="encoding_type",
35 | help="Encoding type for server to utilize",
36 | metavar="ENCODING", default='gzip')
37 | global SERVER_PORT
38 | parser.add_option("-p", "--port", dest="port", default=SERVER_PORT,
39 | help="The port to serve the files on",
40 | metavar="ENCODING")
41 | (options, args) = parser.parse_args()
42 | global encoding_type
43 | encoding_type = options.encoding_type
44 | SERVER_PORT = int(options.port)
45 |
46 | if encoding_type not in ['zlib', 'deflate', 'gzip']:
47 | sys.stderr.write("Please provide a valid encoding_type for the server to utilize.\n")
48 | sys.stderr.write("Possible values are 'zlib', 'gzip', and 'deflate'\n")
49 | sys.stderr.write("Usage: python GzipSimpleHTTPServer.py --encoding=\n")
50 | sys.exit()
51 |
52 |
53 | def zlib_encode(content):
54 | zlib_compress = zlib.compressobj(9, zlib.DEFLATED, zlib.MAX_WBITS)
55 | data = zlib_compress.compress(content) + zlib_compress.flush()
56 | return data
57 |
58 |
59 | def deflate_encode(content):
60 | deflate_compress = zlib.compressobj(9, zlib.DEFLATED, -zlib.MAX_WBITS)
61 | data = deflate_compress.compress(content) + deflate_compress.flush()
62 | return data
63 |
64 |
65 | def gzip_encode(content):
66 | gzip_compress = zlib.compressobj(9, zlib.DEFLATED, zlib.MAX_WBITS | 16)
67 | data = gzip_compress.compress(content) + gzip_compress.flush()
68 | return data
69 |
70 |
71 | class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
72 | """Simple HTTP request handler with GET and HEAD commands.
73 |
74 | This serves files from the current directory and any of its
75 | subdirectories. The MIME type for files is determined by
76 | calling the .guess_type() method.
77 |
78 | The GET and HEAD requests are identical except that the HEAD
79 | request omits the actual contents of the file.
80 |
81 | """
82 |
83 | server_version = "SimpleHTTP/" + __version__
84 |
85 | def do_GET(self):
86 | """Serve a GET request."""
87 | content = self.send_head()
88 | if content:
89 | self.wfile.write(content)
90 |
91 | def do_HEAD(self):
92 | """Serve a HEAD request."""
93 | content = self.send_head()
94 |
95 | def send_head(self):
96 | """Common code for GET and HEAD commands.
97 |
98 | This sends the response code and MIME headers.
99 |
100 | Return value is either a file object (which has to be copied
101 | to the outputfile by the caller unless the command was HEAD,
102 | and must be closed by the caller under all circumstances), or
103 | None, in which case the caller has nothing further to do.
104 |
105 | """
106 | path = self.translate_path(self.path)
107 | print("Serving path '%s'" % path)
108 | f = None
109 | if os.path.isdir(path):
110 | if not self.path.endswith('/'):
111 | # redirect browser - doing basically what apache does
112 | self.send_response(301)
113 | self.send_header("Location", self.path + "/")
114 | self.end_headers()
115 | return None
116 | for index in "index.html", "index.htm":
117 | index = os.path.join(path, index)
118 | if os.path.exists(index):
119 | path = index
120 | break
121 | else:
122 | return self.list_directory(path).read()
123 | ctype = self.guess_type(path)
124 | try:
125 | # Always read in binary mode. Opening files in text mode may cause
126 | # newline translations, making the actual size of the content
127 | # transmitted *less* than the content-length!
128 | f = open(path, 'rb')
129 | except IOError:
130 | self.send_error(404, "File not found")
131 | return None
132 | self.send_response(200)
133 | self.send_header("Content-type", ctype)
134 | self.send_header("Content-Encoding", encoding_type)
135 | fs = os.fstat(f.fileno())
136 | raw_content_length = fs[6]
137 | content = f.read()
138 |
139 | # Encode content based on runtime arg
140 | if encoding_type == "gzip":
141 | content = gzip_encode(content)
142 | elif encoding_type == "deflate":
143 | content = deflate_encode(content)
144 | elif encoding_type == "zlib":
145 | content = zlib_encode(content)
146 |
147 | compressed_content_length = len(content)
148 | f.close()
149 | self.send_header("Content-Length", max(raw_content_length, compressed_content_length))
150 | self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
151 | self.end_headers()
152 | return content
153 |
154 | def list_directory(self, path):
155 | """Helper to produce a directory listing (absent index.html).
156 |
157 | Return value is either a file object, or None (indicating an
158 | error). In either case, the headers are sent, making the
159 | interface the same as for send_head().
160 |
161 | """
162 | try:
163 | list = os.listdir(path)
164 | except os.error:
165 | self.send_error(404, "No permission to list directory")
166 | return None
167 | list.sort(key=lambda a: a.lower())
168 | f = StringIO()
169 | displaypath = cgi.escape(urllib.unquote(self.path))
170 | f.write('')
171 | f.write("\nDirectory listing for %s \n" % displaypath)
172 | f.write("\nDirectory listing for %s \n" % displaypath)
173 | f.write(" \n\n")
174 | for name in list:
175 | fullname = os.path.join(path, name)
176 | displayname = linkname = name
177 | # Append / for directories or @ for symbolic links
178 | if os.path.isdir(fullname):
179 | displayname = name + "/"
180 | linkname = name + "/"
181 | if os.path.islink(fullname):
182 | displayname = name + "@"
183 | # Note: a link to a directory displays with @ and links with /
184 | f.write('%s \n'
185 | % (urllib.quote(linkname), cgi.escape(displayname)))
186 | f.write(" \n \n\n\n")
187 | length = f.tell()
188 | f.seek(0)
189 | self.send_response(200)
190 | encoding = sys.getfilesystemencoding()
191 | self.send_header("Content-type", "text/html; charset=%s" % encoding)
192 | self.send_header("Content-Length", str(length))
193 | self.end_headers()
194 | return f
195 |
196 | def translate_path(self, path):
197 | """Translate a /-separated PATH to the local filename syntax.
198 |
199 | Components that mean special things to the local file system
200 | (e.g. drive or directory names) are ignored. (XXX They should
201 | probably be diagnosed.)
202 |
203 | """
204 | # abandon query parameters
205 | path = path.split('?',1)[0]
206 | path = path.split('#',1)[0]
207 | path = posixpath.normpath(urllib.unquote(path))
208 | words = path.split('/')
209 | words = filter(None, words)
210 | path = os.getcwd()
211 | for word in words:
212 | drive, word = os.path.splitdrive(word)
213 | head, word = os.path.split(word)
214 | if word in (os.curdir, os.pardir): continue
215 | path = os.path.join(path, word)
216 | return path
217 |
218 | def guess_type(self, path):
219 | """Guess the type of a file.
220 |
221 | Argument is a PATH (a filename).
222 |
223 | Return value is a string of the form type/subtype,
224 | usable for a MIME Content-type header.
225 |
226 | The default implementation looks the file's extension
227 | up in the table self.extensions_map, using application/octet-stream
228 | as a default; however it would be permissible (if
229 | slow) to look inside the data to make a better guess.
230 |
231 | """
232 |
233 | base, ext = posixpath.splitext(path)
234 | if ext in self.extensions_map:
235 | return self.extensions_map[ext]
236 | ext = ext.lower()
237 | if ext in self.extensions_map:
238 | return self.extensions_map[ext]
239 | else:
240 | return self.extensions_map['']
241 |
242 | if not mimetypes.inited:
243 | mimetypes.init() # try to read system mime.types
244 | extensions_map = mimetypes.types_map.copy()
245 | extensions_map.update({
246 | '': 'application/octet-stream', # Default
247 | '.py': 'text/plain',
248 | '.c': 'text/plain',
249 | '.h': 'text/plain',
250 | })
251 |
252 |
253 | def test(HandlerClass = SimpleHTTPRequestHandler,
254 | ServerClass = BaseHTTPServer.HTTPServer):
255 | """Run the HTTP request handler class.
256 |
257 | This runs an HTTP server on port 8000 (or the first command line
258 | argument).
259 |
260 | """
261 |
262 | parse_options()
263 |
264 | server_address = ('127.0.0.1', SERVER_PORT)
265 |
266 | SimpleHTTPRequestHandler.protocol_version = "HTTP/1.0"
267 | httpd = BaseHTTPServer.HTTPServer(server_address, SimpleHTTPRequestHandler)
268 |
269 | sa = httpd.socket.getsockname()
270 | print "Serving HTTP on", sa[0], "port", sa[1], "..."
271 | httpd.serve_forever()
272 | BaseHTTPServer.test(HandlerClass, ServerClass)
273 |
274 |
275 | if __name__ == '__main__':
276 | test()
277 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | PWA React Calculator
60 |
83 |
97 |
98 |
99 |
100 | almost there, hang on...
101 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/public/.htaccess:
--------------------------------------------------------------------------------
1 | # Apache Server Configs v3.0.0 | MIT License
2 | # https://github.com/h5bp/server-configs-apache
3 |
4 | # (!) Using `.htaccess` files slows down Apache, therefore, if you have
5 | # access to the main server configuration file (which is usually called
6 | # `httpd.conf`), you should add this logic there.
7 | #
8 | # https://httpd.apache.org/docs/current/howto/htaccess.html
9 |
10 | # ######################################################################
11 | # # CROSS-ORIGIN #
12 | # ######################################################################
13 |
14 | # ----------------------------------------------------------------------
15 | # | Cross-origin requests |
16 | # ----------------------------------------------------------------------
17 |
18 | # Allow cross-origin requests.
19 | #
20 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
21 | # https://enable-cors.org/
22 | # https://www.w3.org/TR/cors/
23 |
24 | #
25 | # Header set Access-Control-Allow-Origin "*"
26 | #
27 |
28 | # ----------------------------------------------------------------------
29 | # | Cross-origin images |
30 | # ----------------------------------------------------------------------
31 |
32 | # Send the CORS header for images when browsers request it.
33 | #
34 | # https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
35 | # https://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html
36 |
37 |
38 |
39 |
40 | SetEnvIf Origin ":" IS_CORS
41 | Header set Access-Control-Allow-Origin "*" env=IS_CORS
42 |
43 |
44 |
45 |
46 | # ----------------------------------------------------------------------
47 | # | Cross-origin web fonts |
48 | # ----------------------------------------------------------------------
49 |
50 | # Allow cross-origin access to web fonts.
51 |
52 |
53 |
54 | Header set Access-Control-Allow-Origin "*"
55 |
56 |
57 |
58 | # ----------------------------------------------------------------------
59 | # | Cross-origin resource timing |
60 | # ----------------------------------------------------------------------
61 |
62 | # Allow cross-origin access to the timing information for all resources.
63 | #
64 | # If a resource isn't served with a `Timing-Allow-Origin` header that
65 | # would allow its timing information to be shared with the document,
66 | # some of the attributes of the `PerformanceResourceTiming` object will
67 | # be set to zero.
68 | #
69 | # https://www.w3.org/TR/resource-timing/
70 | # http://www.stevesouders.com/blog/2014/08/21/resource-timing-practical-tips/
71 |
72 | #
73 | # Header set Timing-Allow-Origin: "*"
74 | #
75 |
76 | # ######################################################################
77 | # # ERRORS #
78 | # ######################################################################
79 |
80 | # ----------------------------------------------------------------------
81 | # | Custom error messages/pages |
82 | # ----------------------------------------------------------------------
83 |
84 | # Customize what Apache returns to the client in case of an error.
85 | # https://httpd.apache.org/docs/current/mod/core.html#errordocument
86 |
87 | ErrorDocument 404 /404.html
88 |
89 | # ----------------------------------------------------------------------
90 | # | Error prevention |
91 | # ----------------------------------------------------------------------
92 |
93 | # Disable the pattern matching based on filenames.
94 | #
95 | # This setting prevents Apache from returning a 404 error as the result
96 | # of a rewrite when the directory with the same name does not exist.
97 | #
98 | # https://httpd.apache.org/docs/current/content-negotiation.html#multiviews
99 |
100 | Options -MultiViews
101 |
102 | # ######################################################################
103 | # # INTERNET EXPLORER #
104 | # ######################################################################
105 |
106 | # ----------------------------------------------------------------------
107 | # | Document modes |
108 | # ----------------------------------------------------------------------
109 |
110 | # Force Internet Explorer 8/9/10 to render pages in the highest mode
111 | # available in the various cases when it may not.
112 | #
113 | # https://hsivonen.fi/doctype/#ie8
114 | #
115 | # (!) Starting with Internet Explorer 11, document modes are deprecated.
116 | # If your business still relies on older web apps and services that were
117 | # designed for older versions of Internet Explorer, you might want to
118 | # consider enabling `Enterprise Mode` throughout your company.
119 | #
120 | # https://msdn.microsoft.com/en-us/library/ie/bg182625.aspx#docmode
121 | # https://blogs.msdn.microsoft.com/ie/2014/04/02/stay-up-to-date-with-enterprise-mode-for-internet-explorer-11/
122 |
123 |
124 |
125 | Header set X-UA-Compatible "IE=edge"
126 |
127 | # `mod_headers` cannot match based on the content-type, however,
128 | # the `X-UA-Compatible` response header should be send only for
129 | # HTML documents and not for the other resources.
130 |
131 |
132 | Header unset X-UA-Compatible
133 |
134 |
135 |
136 |
137 | # ----------------------------------------------------------------------
138 | # | Iframes cookies |
139 | # ----------------------------------------------------------------------
140 |
141 | # Allow cookies to be set from iframes in Internet Explorer.
142 | #
143 | # https://msdn.microsoft.com/en-us/library/ms537343.aspx
144 | # https://www.w3.org/TR/2000/CR-P3P-20001215/
145 |
146 | #
147 | # Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\""
148 | #
149 |
150 | # ######################################################################
151 | # # MEDIA TYPES AND CHARACTER ENCODINGS #
152 | # ######################################################################
153 |
154 | # ----------------------------------------------------------------------
155 | # | Media types |
156 | # ----------------------------------------------------------------------
157 |
158 | # Serve resources with the proper media types (f.k.a. MIME types).
159 | #
160 | # https://www.iana.org/assignments/media-types/media-types.xhtml
161 | # https://httpd.apache.org/docs/current/mod/mod_mime.html#addtype
162 |
163 |
164 |
165 | # Data interchange
166 |
167 | AddType application/atom+xml atom
168 | AddType application/json json map topojson
169 | AddType application/ld+json jsonld
170 | AddType application/rss+xml rss
171 | AddType application/vnd.geo+json geojson
172 | AddType application/xml rdf xml
173 |
174 |
175 | # JavaScript
176 |
177 | # Servers should use text/javascript for JavaScript resources.
178 | # https://html.spec.whatwg.org/multipage/scripting.html#scriptingLanguages
179 |
180 | AddType text/javascript js mjs
181 |
182 |
183 | # Manifest files
184 |
185 | AddType application/manifest+json webmanifest
186 | AddType application/x-web-app-manifest+json webapp
187 | AddType text/cache-manifest appcache
188 |
189 |
190 | # Media files
191 |
192 | AddType audio/mp4 f4a f4b m4a
193 | AddType audio/ogg oga ogg opus
194 | AddType image/bmp bmp
195 | AddType image/svg+xml svg svgz
196 | AddType image/webp webp
197 | AddType video/mp4 f4v f4p m4v mp4
198 | AddType video/ogg ogv
199 | AddType video/webm webm
200 | AddType video/x-flv flv
201 |
202 | # Serving `.ico` image files with a different media type
203 | # prevents Internet Explorer from displaying them as images:
204 | # https://github.com/h5bp/html5-boilerplate/commit/37b5fec090d00f38de64b591bcddcb205aadf8ee
205 |
206 | AddType image/x-icon cur ico
207 |
208 |
209 | # WebAssembly
210 |
211 | AddType application/wasm wasm
212 |
213 |
214 | # Web fonts
215 |
216 | AddType font/woff woff
217 | AddType font/woff2 woff2
218 | AddType application/vnd.ms-fontobject eot
219 | AddType font/ttf ttf
220 | AddType font/collection ttc
221 | AddType font/otf otf
222 |
223 |
224 | # Other
225 |
226 | AddType application/octet-stream safariextz
227 | AddType application/x-bb-appworld bbaw
228 | AddType application/x-chrome-extension crx
229 | AddType application/x-opera-extension oex
230 | AddType application/x-xpinstall xpi
231 | AddType text/calendar ics
232 | AddType text/markdown markdown md
233 | AddType text/vcard vcard vcf
234 | AddType text/vnd.rim.location.xloc xloc
235 | AddType text/vtt vtt
236 | AddType text/x-component htc
237 |
238 |
239 |
240 | # ----------------------------------------------------------------------
241 | # | Character encodings |
242 | # ----------------------------------------------------------------------
243 |
244 | # Serve all resources labeled as `text/html` or `text/plain`
245 | # with the media type `charset` parameter set to `UTF-8`.
246 | #
247 | # https://httpd.apache.org/docs/current/mod/core.html#adddefaultcharset
248 |
249 | AddDefaultCharset utf-8
250 |
251 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
252 |
253 | # Serve the following file types with the media type `charset`
254 | # parameter set to `UTF-8`.
255 | #
256 | # https://httpd.apache.org/docs/current/mod/mod_mime.html#addcharset
257 |
258 |
259 | AddCharset utf-8 .atom \
260 | .bbaw \
261 | .css \
262 | .geojson \
263 | .ics \
264 | .js \
265 | .json \
266 | .jsonld \
267 | .manifest \
268 | .markdown \
269 | .md \
270 | .mjs \
271 | .rdf \
272 | .rss \
273 | .topojson \
274 | .vtt \
275 | .webapp \
276 | .webmanifest \
277 | .xloc \
278 | .xml
279 |
280 |
281 | # ######################################################################
282 | # # REWRITES #
283 | # ######################################################################
284 |
285 | # ----------------------------------------------------------------------
286 | # | Rewrite engine |
287 | # ----------------------------------------------------------------------
288 |
289 | # (1) Turn on the rewrite engine (this is necessary in order for
290 | # the `RewriteRule` directives to work).
291 | #
292 | # https://httpd.apache.org/docs/current/mod/mod_rewrite.html#RewriteEngine
293 | #
294 | # (2) Enable the `FollowSymLinks` option if it isn't already.
295 | #
296 | # https://httpd.apache.org/docs/current/mod/core.html#options
297 | #
298 | # (3) If your web host doesn't allow the `FollowSymlinks` option,
299 | # you need to comment it out or remove it, and then uncomment
300 | # the `Options +SymLinksIfOwnerMatch` line (4), but be aware
301 | # of the performance impact.
302 | #
303 | # https://httpd.apache.org/docs/current/misc/perf-tuning.html#symlinks
304 | #
305 | # (4) Some cloud hosting services will require you set `RewriteBase`.
306 | #
307 | # https://www.rackspace.com/knowledge_center/frequently-asked-question/why-is-modrewrite-not-working-on-my-site
308 | # https://httpd.apache.org/docs/current/mod/mod_rewrite.html#rewritebase
309 | #
310 | # (5) Depending on how your server is set up, you may also need to
311 | # use the `RewriteOptions` directive to enable some options for
312 | # the rewrite engine.
313 | #
314 | # https://httpd.apache.org/docs/current/mod/mod_rewrite.html#rewriteoptions
315 | #
316 | # (6) Set %{ENV:PROTO} variable, to allow rewrites to redirect with the
317 | # appropriate schema automatically (http or https).
318 |
319 |
320 |
321 | # (1)
322 | RewriteEngine On
323 |
324 | # (2)
325 | Options +FollowSymlinks
326 |
327 | # (3)
328 | # Options +SymLinksIfOwnerMatch
329 |
330 | # (4)
331 | # RewriteBase /
332 |
333 | # (5)
334 | # RewriteOptions
335 |
336 | # (6)
337 | RewriteCond %{HTTPS} =on
338 | RewriteRule ^ - [env=proto:https]
339 | RewriteCond %{HTTPS} !=on
340 | RewriteRule ^ - [env=proto:http]
341 |
342 |
343 |
344 | # ----------------------------------------------------------------------
345 | # | Forcing `https://` |
346 | # ----------------------------------------------------------------------
347 |
348 | # Redirect from the `http://` to the `https://` version of the URL.
349 | # https://wiki.apache.org/httpd/RewriteHTTPToHTTPS
350 |
351 |
352 | RewriteEngine On
353 | RewriteCond %{HTTPS} !=on
354 | RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
355 |
356 |
357 | # ----------------------------------------------------------------------
358 | # | Suppressing the `www.` at the beginning of URLs |
359 | # ----------------------------------------------------------------------
360 |
361 | # Rewrite www.example.com → example.com
362 |
363 | # The same content should never be available under two different
364 | # URLs, especially not with and without `www.` at the beginning.
365 | # This can cause SEO problems (duplicate content), and therefore,
366 | # you should choose one of the alternatives and redirect the other
367 | # one.
368 | #
369 | # (!) NEVER USE BOTH WWW-RELATED RULES AT THE SAME TIME!
370 |
371 | # (1) The rule assume by default that both HTTP and HTTPS
372 | # environments are available for redirection.
373 | # If your SSL certificate could not handle one of the domains
374 | # used during redirection, you should turn the condition on.
375 | #
376 | # https://github.com/h5bp/server-configs-apache/issues/52
377 |
378 |
379 | RewriteEngine On
380 | # (1)
381 | # RewriteCond %{HTTPS} !=on
382 | RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
383 | RewriteRule ^ %{ENV:PROTO}://%1%{REQUEST_URI} [R=301,L]
384 |
385 |
386 | # ----------------------------------------------------------------------
387 | # | Forcing the `www.` at the beginning of URLs |
388 | # ----------------------------------------------------------------------
389 |
390 | # Rewrite example.com → www.example.com
391 |
392 | # The same content should never be available under two different
393 | # URLs, especially not with and without `www.` at the beginning.
394 | # This can cause SEO problems (duplicate content), and therefore,
395 | # you should choose one of the alternatives and redirect the other
396 | # one.
397 | #
398 | # (!) NEVER USE BOTH WWW-RELATED RULES AT THE SAME TIME!
399 |
400 | # (1) The rule assume by default that both HTTP and HTTPS
401 | # environments are available for redirection.
402 | # If your SSL certificate could not handle one of the domains
403 | # used during redirection, you should turn the condition on.
404 | #
405 | # https://github.com/h5bp/server-configs-apache/issues/52
406 |
407 | # Be aware that the following might not be a good idea if you use "real"
408 | # subdomains for certain parts of your website.
409 |
410 | #
411 | # RewriteEngine On
412 | # # (1)
413 | # # RewriteCond %{HTTPS} !=on
414 | # RewriteCond %{HTTP_HOST} !^www\. [NC]
415 | # RewriteCond %{SERVER_ADDR} !=127.0.0.1
416 | # RewriteCond %{SERVER_ADDR} !=::1
417 | # RewriteRule ^ %{ENV:PROTO}://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
418 | #
419 |
420 | # ######################################################################
421 | # # SECURITY #
422 | # ######################################################################
423 |
424 | # ----------------------------------------------------------------------
425 | # | Clickjacking |
426 | # ----------------------------------------------------------------------
427 |
428 | # Protect website against clickjacking.
429 | #
430 | # The example below sends the `X-Frame-Options` response header with
431 | # the value `DENY`, informing browsers not to display the content of
432 | # the web page in any frame.
433 | #
434 | # This might not be the best setting for everyone. You should read
435 | # about the other two possible values the `X-Frame-Options` header
436 | # field can have: `SAMEORIGIN` and `ALLOW-FROM`.
437 | # https://tools.ietf.org/html/rfc7034#section-2.1.
438 | #
439 | # Keep in mind that while you could send the `X-Frame-Options` header
440 | # for all of your website’s pages, this has the potential downside that
441 | # it forbids even non-malicious framing of your content (e.g.: when
442 | # users visit your website using a Google Image Search results page).
443 | #
444 | # Nonetheless, you should ensure that you send the `X-Frame-Options`
445 | # header for all pages that allow a user to make a state changing
446 | # operation (e.g: pages that contain one-click purchase links, checkout
447 | # or bank-transfer confirmation pages, pages that make permanent
448 | # configuration changes, etc.).
449 | #
450 | # Sending the `X-Frame-Options` header can also protect your website
451 | # against more than just clickjacking attacks:
452 | # https://cure53.de/xfo-clickjacking.pdf.
453 | #
454 | # https://tools.ietf.org/html/rfc7034
455 | # https://blogs.msdn.microsoft.com/ieinternals/2010/03/30/combating-clickjacking-with-x-frame-options/
456 | # https://www.owasp.org/index.php/Clickjacking
457 |
458 | #
459 |
460 | # Header set X-Frame-Options "DENY"
461 |
462 | # # `mod_headers` cannot match based on the content-type, however,
463 | # # the `X-Frame-Options` response header should be send only for
464 | # # HTML documents and not for the other resources.
465 |
466 | #
467 | # Header unset X-Frame-Options
468 | #
469 |
470 | #
471 |
472 | # ----------------------------------------------------------------------
473 | # | Content Security Policy (CSP) |
474 | # ----------------------------------------------------------------------
475 |
476 | # Mitigate the risk of cross-site scripting and other content-injection
477 | # attacks.
478 | #
479 | # This can be done by setting a `Content Security Policy` which
480 | # whitelists trusted sources of content for your website.
481 | #
482 | # The example header below allows ONLY scripts that are loaded from
483 | # the current website's origin (no inline scripts, no CDN, etc).
484 | # That almost certainly won't work as-is for your website!
485 | #
486 | # To make things easier, you can use an online CSP header generator
487 | # such as: http://cspisawesome.com/.
488 | #
489 | # https://content-security-policy.com/
490 | # https://www.html5rocks.com/en/tutorials/security/content-security-policy/
491 | # https://w3c.github.io/webappsec-csp/
492 |
493 |
494 | Header set Content-Security-Policy "default-src 'none'; manifest-src 'self'; script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com/ajax/libs/rollbar.js/2.4.6/rollbar.min.js 'unsafe-eval' www.google-analytics.com; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' www.google-analytics.com stats.g.doubleclick.net; media-src 'self'; frame-src 'self'; font-src 'self'; connect-src 'self' https://api.rollbar.com/api/1/item/"
495 |
496 | # `mod_headers` cannot match based on the content-type, however,
497 | # the `Content-Security-Policy` response header should be send
498 | # only for HTML documents and not for the other resources.
499 |
500 |
501 | Header unset Content-Security-Policy
502 |
503 |
504 |
505 |
506 | # ----------------------------------------------------------------------
507 | # | File access |
508 | # ----------------------------------------------------------------------
509 |
510 | # Block access to directories without a default document.
511 | #
512 | # You should leave the following uncommented, as you shouldn't allow
513 | # anyone to surf through every directory on your server (which may
514 | # includes rather private places such as the CMS's directories).
515 |
516 |
517 | Options -Indexes
518 |
519 |
520 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
521 |
522 | # Block access to all hidden files and directories with the exception of
523 | # the visible content from within the `/.well-known/` hidden directory.
524 | #
525 | # These types of files usually contain user preferences or the preserved
526 | # state of an utility, and can include rather private places like, for
527 | # example, the `.git` or `.svn` directories.
528 | #
529 | # The `/.well-known/` directory represents the standard (RFC 5785) path
530 | # prefix for "well-known locations" (e.g.: `/.well-known/manifest.json`,
531 | # `/.well-known/keybase.txt`), and therefore, access to its visible
532 | # content should not be blocked.
533 | #
534 | # https://www.mnot.net/blog/2010/04/07/well-known
535 | # https://tools.ietf.org/html/rfc5785
536 |
537 |
538 | RewriteEngine On
539 | RewriteCond %{REQUEST_URI} "!(^|/)\.well-known/([^./]+./?)+$" [NC]
540 | RewriteCond %{SCRIPT_FILENAME} -d [OR]
541 | RewriteCond %{SCRIPT_FILENAME} -f
542 | RewriteRule "(^|/)\." - [F]
543 |
544 |
545 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
546 |
547 | # Block access to files that can expose sensitive information.
548 | #
549 | # By default, block access to backup and source files that may be
550 | # left by some text editors and can pose a security risk when anyone
551 | # has access to them.
552 | #
553 | # https://feross.org/cmsploit/
554 | #
555 | # (!) Update the `` regular expression from below to
556 | # include any files that might end up on your production server and
557 | # can expose sensitive information about your website. These files may
558 | # include: configuration files, files that contain metadata about the
559 | # project (e.g.: project dependencies), build scripts, etc..
560 |
561 |
562 |
563 | Require all denied
564 |
565 |
566 |
567 | # ----------------------------------------------------------------------
568 | # | HTTP Strict Transport Security (HSTS) |
569 | # ----------------------------------------------------------------------
570 |
571 | # Force client-side SSL redirection.
572 | #
573 | # If a user types `example.com` in their browser, even if the server
574 | # redirects them to the secure version of the website, that still leaves
575 | # a window of opportunity (the initial HTTP connection) for an attacker
576 | # to downgrade or redirect the request.
577 | #
578 | # The following header ensures that browser will ONLY connect to your
579 | # server via HTTPS, regardless of what the users type in the browser's
580 | # address bar.
581 | #
582 | # (!) Be aware that this, once published, is not revokable and you must ensure
583 | # being able to serve the site via SSL for the duration you've specified
584 | # in max-age. When you don't have a valid SSL connection (anymore) your
585 | # visitors will see a nasty error message even when attempting to connect
586 | # via simple HTTP.
587 | #
588 | # (!) Remove the `includeSubDomains` optional directive if the website's
589 | # subdomains are not using HTTPS.
590 | #
591 | # (1) If you want to submit your site for HSTS preload (2) you must
592 | # * ensure the `includeSubDomains` directive to be present
593 | # * the `preload` directive to be specified
594 | # * the `max-age` to be at least 31536000 seconds (1 year) according to the current status.
595 | #
596 | # It is also advised (3) to only serve the HSTS header via a secure connection
597 | # which can be done with either `env=https` or `"expr=%{HTTPS} == 'on'"` (4). The
598 | # exact way depends on your environment and might just be tried.
599 | #
600 | # https://www.html5rocks.com/en/tutorials/security/transport-layer-security/
601 | # https://tools.ietf.org/html/rfc6797#section-6.1
602 | # https://blogs.msdn.microsoft.com/ieinternals/2014/08/18/strict-transport-security/
603 | # (2) https://hstspreload.org/
604 | # (3) https://tools.ietf.org/html/rfc6797#section-7.2
605 | # (4) https://stackoverflow.com/questions/24144552/how-to-set-hsts-header-from-htaccess-only-on-https/24145033#comment81632711_24145033
606 |
607 |
608 | Header always set Strict-Transport-Security "max-age=16070400; includeSubDomains; preload"
609 | # (1) or if HSTS preloading is desired (respect (2) for current requirements):
610 | # Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" env=HTTPS
611 | # (4) respectively… (respect (2) for current requirements):
612 | # Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" "expr=%{HTTPS} == 'on'"
613 |
614 |
615 | # ----------------------------------------------------------------------
616 | # | Reducing MIME type security risks |
617 | # ----------------------------------------------------------------------
618 |
619 | # Prevent some browsers from MIME-sniffing the response.
620 | #
621 | # This reduces exposure to drive-by download attacks and cross-origin
622 | # data leaks, and should be left uncommented, especially if the server
623 | # is serving user-uploaded content or content that could potentially be
624 | # treated as executable by the browser.
625 | #
626 | # https://www.slideshare.net/hasegawayosuke/owasp-hasegawa
627 | # https://blogs.msdn.microsoft.com/ie/2008/07/02/ie8-security-part-v-comprehensive-protection/
628 | # https://msdn.microsoft.com/en-us/library/ie/gg622941.aspx
629 | # https://mimesniff.spec.whatwg.org/
630 |
631 |
632 | Header set X-Content-Type-Options "nosniff"
633 |
634 |
635 | # ----------------------------------------------------------------------
636 | # | Reflected Cross-Site Scripting (XSS) attacks |
637 | # ----------------------------------------------------------------------
638 |
639 | # (1) Try to re-enable the cross-site scripting (XSS) filter built
640 | # into most web browsers.
641 | #
642 | # The filter is usually enabled by default, but in some cases it
643 | # may be disabled by the user. However, in Internet Explorer for
644 | # example, it can be re-enabled just by sending the
645 | # `X-XSS-Protection` header with the value of `1`.
646 | #
647 | # (2) Prevent web browsers from rendering the web page if a potential
648 | # reflected (a.k.a non-persistent) XSS attack is detected by the
649 | # filter.
650 | #
651 | # By default, if the filter is enabled and browsers detect a
652 | # reflected XSS attack, they will attempt to block the attack
653 | # by making the smallest possible modifications to the returned
654 | # web page.
655 | #
656 | # Unfortunately, in some browsers (e.g.: Internet Explorer),
657 | # this default behavior may allow the XSS filter to be exploited,
658 | # thereby, it's better to inform browsers to prevent the rendering
659 | # of the page altogether, instead of attempting to modify it.
660 | #
661 | # https://hackademix.net/2009/11/21/ies-xss-filter-creates-xss-vulnerabilities
662 | #
663 | # (!) Do not rely on the XSS filter to prevent XSS attacks! Ensure that
664 | # you are taking all possible measures to prevent XSS attacks, the
665 | # most obvious being: validating and sanitizing your website's inputs.
666 | #
667 | # https://blogs.msdn.microsoft.com/ie/2008/07/02/ie8-security-part-iv-the-xss-filter/
668 | # https://blogs.msdn.microsoft.com/ieinternals/2011/01/31/controlling-the-xss-filter/
669 | # https://www.owasp.org/index.php/Cross-site_Scripting_%28XSS%29
670 |
671 |
672 |
673 | # (1) (2)
674 | Header set X-XSS-Protection "1; mode=block"
675 |
676 | # `mod_headers` cannot match based on the content-type, however,
677 | # the `X-XSS-Protection` response header should be send only for
678 | # HTML documents and not for the other resources.
679 |
680 |
681 | Header unset X-XSS-Protection
682 |
683 |
684 |
685 |
686 | # ----------------------------------------------------------------------
687 | # | Referrer Policy |
688 | # ----------------------------------------------------------------------
689 |
690 | # A web application uses HTTPS and a URL-based session identifier.
691 | # The web application might wish to link to HTTPS resources on other
692 | # web sites without leaking the user's session identifier in the URL.
693 | #
694 | # This can be done by setting a `Referrer Policy` which
695 | # whitelists trusted sources of content for your website.
696 | #
697 | # To check your referrer policy, you can use an online service
698 | # such as: https://securityheaders.io/.
699 | #
700 | # https://scotthelme.co.uk/a-new-security-header-referrer-policy/
701 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
702 |
703 | #
704 |
705 | # # no-referrer-when-downgrade (default)
706 | # # This should be the user agent's default behavior if no policy is
707 | # # specified.The origin is sent as referrer to a-priori as-much-secure
708 | # # destination (HTTPS->HTTPS), but isn't sent to a less secure destination
709 | # # (HTTPS->HTTP).
710 |
711 | # Header set Referrer-Policy "no-referrer-when-downgrade"
712 |
713 | # # `mod_headers` cannot match based on the content-type, however,
714 | # # the `Referrer-Policy` response header should be send
715 | # # only for HTML documents and not for the other resources.
716 |
717 | #
718 | # Header unset Referrer-Policy
719 | #
720 |
721 | #
722 |
723 | # ----------------------------------------------------------------------
724 | # | Server-side technology information |
725 | # ----------------------------------------------------------------------
726 |
727 | # Remove the `X-Powered-By` response header that:
728 | #
729 | # * is set by some frameworks and server-side languages
730 | # (e.g.: ASP.NET, PHP), and its value contains information
731 | # about them (e.g.: their name, version number)
732 | #
733 | # * doesn't provide any value to users, contributes to header
734 | # bloat, and in some cases, the information it provides can
735 | # expose vulnerabilities
736 | #
737 | # (!) If you can, you should disable the `X-Powered-By` header from the
738 | # language / framework level (e.g.: for PHP, you can do that by setting
739 | # `expose_php = off` in `php.ini`)
740 | #
741 | # https://php.net/manual/en/ini.core.php#ini.expose-php
742 |
743 |
744 | Header unset X-Powered-By
745 |
746 |
747 | # ----------------------------------------------------------------------
748 | # | Server software information |
749 | # ----------------------------------------------------------------------
750 |
751 | # Prevent Apache from adding a trailing footer line containing
752 | # information about the server to the server-generated documents
753 | # (e.g.: error messages, directory listings, etc.)
754 | #
755 | # https://httpd.apache.org/docs/current/mod/core.html#serversignature
756 |
757 | ServerSignature Off
758 |
759 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
760 |
761 | # Prevent Apache from sending in the `Server` response header its
762 | # exact version number, the description of the generic OS-type or
763 | # information about its compiled-in modules.
764 | #
765 | # (!) The `ServerTokens` directive will only work in the main server
766 | # configuration file, so don't try to enable it in the `.htaccess` file!
767 | #
768 | # https://httpd.apache.org/docs/current/mod/core.html#servertokens
769 |
770 | #ServerTokens Prod
771 |
772 | # ######################################################################
773 | # # WEB PERFORMANCE #
774 | # ######################################################################
775 |
776 | # ----------------------------------------------------------------------
777 | # | Compression |
778 | # ----------------------------------------------------------------------
779 |
780 |
781 |
782 | # Force compression for mangled `Accept-Encoding` request headers
783 | # https://developer.yahoo.com/blogs/ydn/pushing-beyond-gzipping-25601.html
784 |
785 |
786 |
787 | SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding
788 | RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding
789 |
790 |
791 |
792 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
793 |
794 | # Compress all output labeled with one of the following media types.
795 | # https://httpd.apache.org/docs/current/mod/mod_filter.html#addoutputfilterbytype
796 |
797 |
798 | AddOutputFilterByType DEFLATE "application/atom+xml" \
799 | "application/javascript" \
800 | "application/json" \
801 | "application/ld+json" \
802 | "application/manifest+json" \
803 | "application/rdf+xml" \
804 | "application/rss+xml" \
805 | "application/schema+json" \
806 | "application/vnd.geo+json" \
807 | "application/vnd.ms-fontobject" \
808 | "application/wasm" \
809 | "application/x-font-ttf" \
810 | "application/x-javascript" \
811 | "application/x-web-app-manifest+json" \
812 | "application/xhtml+xml" \
813 | "application/xml" \
814 | "font/collection" \
815 | "font/eot" \
816 | "font/opentype" \
817 | "font/otf" \
818 | "font/ttf" \
819 | "image/bmp" \
820 | "image/svg+xml" \
821 | "image/vnd.microsoft.icon" \
822 | "image/x-icon" \
823 | "text/cache-manifest" \
824 | "text/calendar" \
825 | "text/css" \
826 | "text/html" \
827 | "text/javascript" \
828 | "text/plain" \
829 | "text/markdown" \
830 | "text/vcard" \
831 | "text/vnd.rim.location.xloc" \
832 | "text/vtt" \
833 | "text/x-component" \
834 | "text/x-cross-domain-policy" \
835 | "text/xml"
836 |
837 |
838 |
839 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
840 |
841 | # Map the following filename extensions to the specified
842 | # encoding type in order to make Apache serve the file types
843 | # with the appropriate `Content-Encoding` response header
844 | # (do note that this will NOT make Apache compress them!).
845 | #
846 | # If these files types would be served without an appropriate
847 | # `Content-Enable` response header, client applications (e.g.:
848 | # browsers) wouldn't know that they first need to uncompress
849 | # the response, and thus, wouldn't be able to understand the
850 | # content.
851 | #
852 | # https://httpd.apache.org/docs/current/mod/mod_mime.html#addencoding
853 |
854 |
855 | AddEncoding gzip svgz
856 |
857 |
858 |
859 |
860 | # ----------------------------------------------------------------------
861 | # | Brotli pre-compressed content |
862 | # ----------------------------------------------------------------------
863 |
864 | # Serve brotli compressed CSS, JS, HTML, SVG, ICS and JSON files
865 | # if they exist and if the client accepts br encoding.
866 | #
867 | # (!) To make this part relevant, you need to generate encoded
868 | # files by your own. Enabling this part will not auto-generate
869 | # brotlied files.
870 | #
871 | # https://httpd.apache.org/docs/current/mod/mod_brotli.html#precompressed
872 |
873 |
874 |
875 | RewriteCond %{HTTP:Accept-Encoding} br
876 | RewriteCond %{REQUEST_FILENAME}\.br -f
877 | RewriteRule \.(css|ics|js|json|html|svg)$ %{REQUEST_URI}.br [L]
878 |
879 | # Prevent mod_deflate double gzip
880 | RewriteRule \.br$ - [E=no-gzip:1]
881 |
882 |
883 |
884 |
885 | # Serve correct content types
886 | AddType text/css css.br
887 | AddType text/calendar ics.br
888 | AddType text/javascript js.br
889 | AddType application/json json.br
890 | AddType text/html html.br
891 | AddType image/svg+xml svg.br
892 |
893 | # Serve correct content charset
894 | AddCharset utf-8 .css.br \
895 | .ics.br \
896 | .js.br \
897 | .json.br
898 |
899 |
900 | # Force proxies to cache brotlied and non-brotlied files separately
901 | Header append Vary Accept-Encoding
902 |
903 |
904 |
905 | # Serve correct encoding type
906 | AddEncoding br .br
907 |
908 |
909 |
910 | # ----------------------------------------------------------------------
911 | # | GZip pre-compressed content |
912 | # ----------------------------------------------------------------------
913 |
914 | # Serve gzip compressed CSS, JS, HTML, SVG, ICS and JSON files
915 | # if they exist and if the client accepts gzip encoding.
916 | #
917 | # (!) To make this part relevant, you need to generate encoded
918 | # files by your own. Enabling this part will not auto-generate
919 | # gziped files.
920 | #
921 | # https://httpd.apache.org/docs/current/mod/mod_deflate.html#precompressed
922 | #
923 | # (1)
924 | # Removing default MIME Type for .gz files allowing to add custom
925 | # sub-types.
926 | # You may prefer using less generic extensions such as .html_gz in
927 | # order to keep default behavior regarding .gz files.
928 | # https://httpd.apache.org/docs/current/mod/mod_mime.html#removetype
929 |
930 | #
931 |
932 | # RewriteCond %{HTTP:Accept-Encoding} gzip
933 | # RewriteCond %{REQUEST_FILENAME}\.gz -f
934 | # RewriteRule \.(css|ics|js|json|html|svg)$ %{REQUEST_URI}.gz [L]
935 |
936 | # # Prevent mod_deflate double gzip
937 | # RewriteRule \.gz$ - [E=no-gzip:1]
938 |
939 | #
940 |
941 | # # Serve correct content types
942 | #
943 | # # (1)
944 | # RemoveType gz
945 |
946 | # # Serve correct content types
947 | # AddType text/css css.gz
948 | # AddType text/calendar ics.gz
949 | # AddType text/javascript js.gz
950 | # AddType application/json json.gz
951 | # AddType text/html html.gz
952 | # AddType image/svg+xml svg.gz
953 |
954 | # # Serve correct content charset
955 | # AddCharset utf-8 .css.gz \
956 | # .ics.gz \
957 | # .js.gz \
958 | # .json.gz
959 | #
960 |
961 | # # Force proxies to cache gzipped and non-gzipped files separately
962 | # Header append Vary Accept-Encoding
963 |
964 | #
965 |
966 | # # Serve correct encoding type
967 | # AddEncoding gzip .gz
968 |
969 | #
970 |
971 | # ----------------------------------------------------------------------
972 | # | Content transformation |
973 | # ----------------------------------------------------------------------
974 |
975 | # Prevent intermediate caches or proxies (e.g.: such as the ones
976 | # used by mobile network providers) from modifying the website's
977 | # content.
978 | #
979 | # https://tools.ietf.org/html/rfc2616#section-14.9.5
980 | #
981 | # (!) If you are using `mod_pagespeed`, please note that setting
982 | # the `Cache-Control: no-transform` response header will prevent
983 | # `PageSpeed` from rewriting `HTML` files, and, if the
984 | # `ModPagespeedDisableRewriteOnNoTransform` directive isn't set
985 | # to `off`, also from rewriting other resources.
986 | #
987 | # https://developers.google.com/speed/pagespeed/module/configuration#notransform
988 |
989 | #
990 | # Header merge Cache-Control "no-transform"
991 | #
992 |
993 | # ----------------------------------------------------------------------
994 | # | ETags |
995 | # ----------------------------------------------------------------------
996 |
997 | # Remove `ETags` as resources are sent with far-future expires headers.
998 | #
999 | # https://developer.yahoo.com/performance/rules.html#etags
1000 | # https://tools.ietf.org/html/rfc7232#section-2.3
1001 |
1002 | # `FileETag None` doesn't work in all cases.
1003 |
1004 | Header unset ETag
1005 |
1006 |
1007 | FileETag None
1008 |
1009 | # ----------------------------------------------------------------------
1010 | # | Expires headers |
1011 | # ----------------------------------------------------------------------
1012 |
1013 | # Serve resources with far-future expires headers.
1014 | #
1015 | # (!) If you don't control versioning with filename-based
1016 | # cache busting, you should consider lowering the cache times
1017 | # to something like one week.
1018 | #
1019 | # https://httpd.apache.org/docs/current/mod/mod_expires.html
1020 |
1021 |
1022 |
1023 | ExpiresActive on
1024 | ExpiresDefault "access plus 1 year"
1025 |
1026 | # CSS
1027 |
1028 | ExpiresByType text/css "access plus 1 year"
1029 |
1030 |
1031 | # Data interchange
1032 |
1033 | ExpiresByType application/atom+xml "access plus 1 hour"
1034 | ExpiresByType application/rdf+xml "access plus 1 hour"
1035 | ExpiresByType application/rss+xml "access plus 1 hour"
1036 |
1037 | ExpiresByType application/json "access plus 0 seconds"
1038 | ExpiresByType application/ld+json "access plus 0 seconds"
1039 | ExpiresByType application/schema+json "access plus 0 seconds"
1040 | ExpiresByType application/vnd.geo+json "access plus 0 seconds"
1041 | ExpiresByType application/xml "access plus 0 seconds"
1042 | ExpiresByType text/calendar "access plus 0 seconds"
1043 | ExpiresByType text/xml "access plus 0 seconds"
1044 |
1045 |
1046 | # Favicon (cannot be renamed!) and cursor images
1047 |
1048 | ExpiresByType image/vnd.microsoft.icon "access plus 1 week"
1049 | ExpiresByType image/x-icon "access plus 1 week"
1050 |
1051 | # HTML
1052 |
1053 | ExpiresByType text/html "access plus 0 seconds"
1054 |
1055 |
1056 | # JavaScript
1057 |
1058 | ExpiresByType application/javascript "access plus 1 year"
1059 | ExpiresByType application/x-javascript "access plus 1 year"
1060 | ExpiresByType text/javascript "access plus 1 year"
1061 |
1062 |
1063 | # Manifest files
1064 |
1065 | ExpiresByType application/manifest+json "access plus 1 week"
1066 | ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds"
1067 | ExpiresByType text/cache-manifest "access plus 0 seconds"
1068 |
1069 |
1070 | # Markdown
1071 |
1072 | ExpiresByType text/markdown "access plus 0 seconds"
1073 |
1074 |
1075 | # Media files
1076 |
1077 | ExpiresByType audio/ogg "access plus 1 month"
1078 | ExpiresByType image/bmp "access plus 1 month"
1079 | ExpiresByType image/gif "access plus 1 month"
1080 | ExpiresByType image/jpeg "access plus 1 month"
1081 | ExpiresByType image/png "access plus 1 month"
1082 | ExpiresByType image/svg+xml "access plus 1 month"
1083 | ExpiresByType image/webp "access plus 1 month"
1084 | ExpiresByType video/mp4 "access plus 1 month"
1085 | ExpiresByType video/ogg "access plus 1 month"
1086 | ExpiresByType video/webm "access plus 1 month"
1087 |
1088 |
1089 | # WebAssembly
1090 |
1091 | ExpiresByType application/wasm "access plus 1 year"
1092 |
1093 |
1094 | # Web fonts
1095 |
1096 | # Collection
1097 | ExpiresByType font/collection "access plus 1 month"
1098 |
1099 | # Embedded OpenType (EOT)
1100 | ExpiresByType application/vnd.ms-fontobject "access plus 1 month"
1101 | ExpiresByType font/eot "access plus 1 month"
1102 |
1103 | # OpenType
1104 | ExpiresByType font/opentype "access plus 1 month"
1105 | ExpiresByType font/otf "access plus 1 month"
1106 |
1107 | # TrueType
1108 | ExpiresByType application/x-font-ttf "access plus 1 month"
1109 | ExpiresByType font/ttf "access plus 1 month"
1110 |
1111 | # Web Open Font Format (WOFF) 1.0
1112 | ExpiresByType application/font-woff "access plus 1 month"
1113 | ExpiresByType application/x-font-woff "access plus 1 month"
1114 | ExpiresByType font/woff "access plus 1 month"
1115 |
1116 | # Web Open Font Format (WOFF) 2.0
1117 | ExpiresByType application/font-woff2 "access plus 1 month"
1118 | ExpiresByType font/woff2 "access plus 1 month"
1119 |
1120 |
1121 | # Other
1122 |
1123 | ExpiresByType text/x-cross-domain-policy "access plus 1 week"
1124 |
1125 |
1126 |
1127 | # ----------------------------------------------------------------------
1128 | # | File concatenation |
1129 | # ----------------------------------------------------------------------
1130 |
1131 | # Allow concatenation from within specific files.
1132 | #
1133 | # e.g.:
1134 | #
1135 | # If you have the following lines in a file called, for
1136 | # example, `main.combined.js`:
1137 | #
1138 | #
1139 | #
1140 | #
1141 | # Apache will replace those lines with the content of the
1142 | # specified files.
1143 |
1144 | #
1145 | #
1146 | # Options +Includes
1147 | # AddOutputFilterByType INCLUDES application/javascript \
1148 | # application/x-javascript \
1149 | # text/javascript
1150 | # SetOutputFilter INCLUDES
1151 | #
1152 | #
1153 | # Options +Includes
1154 | # AddOutputFilterByType INCLUDES text/css
1155 | # SetOutputFilter INCLUDES
1156 | #
1157 | #
1158 |
1159 | # ----------------------------------------------------------------------
1160 | # | Filename-based cache busting |
1161 | # ----------------------------------------------------------------------
1162 |
1163 | # If you're not using a build process to manage your filename version
1164 | # revving, you might want to consider enabling the following directives
1165 | # to route all requests such as `/style.12345.css` to `/style.css`.
1166 | #
1167 | # To understand why this is important and even a better solution than
1168 | # using something like `*.css?v231`, please see:
1169 | # http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/
1170 |
1171 | #
1172 | # RewriteEngine On
1173 | # RewriteCond %{REQUEST_FILENAME} !-f
1174 | # RewriteRule ^(.+)\.(\d+)\.(bmp|css|cur|gif|ico|jpe?g|m?js|png|svgz?|webp|webmanifest)$ $1.$3 [L]
1175 | #
1176 |
--------------------------------------------------------------------------------