├── .circleci └── config.yml ├── .editorconfig ├── .gitattributes ├── .gitignore ├── bsconfig.json ├── changelog.md ├── license ├── now.json ├── package.json ├── public └── index.html ├── readme.md ├── src ├── __tests__ │ └── store_test.re ├── components │ ├── App.re │ ├── Button.re │ ├── Checkmark.re │ ├── Feature.re │ ├── Features.re │ ├── Hero.re │ ├── Root.re │ ├── Soon.re │ ├── Svg.re │ └── __tests__ │ │ └── App_test.re ├── globalStyles.re ├── index.re └── store.re └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:10 6 | steps: 7 | - checkout 8 | 9 | - restore_cache: 10 | name: Restore node_modules cache 11 | keys: 12 | - v1-node-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }} 13 | - v1-node-{{ arch }}-{{ .Branch }}- 14 | - v1-node-{{ arch }}- 15 | 16 | - run: 17 | name: Nodejs Version 18 | command: node --version 19 | 20 | - run: 21 | name: Yarn Version 22 | command: yarn --version 23 | 24 | - run: 25 | name: Install Packages 26 | command: yarn install --frozen-lockfile 27 | 28 | - run: 29 | name: Run Tests 30 | command: yarn test --maxWorkers=2 31 | 32 | - save_cache: 33 | name: Save node_modules cache 34 | key: v1-node-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }} 35 | paths: 36 | - node_modules 37 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | *.re linguist-language=OCaml 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Build 5 | .cache 6 | dist 7 | lib 8 | .bsb.lock 9 | .merlin 10 | **/*.bs.js 11 | 12 | # Logs 13 | *.log 14 | 15 | # macOS 16 | .*DS_Store 17 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reason-calculator", 3 | "reason": { 4 | "react-jsx": 3 5 | }, 6 | "bs-dependencies": ["bs-css", "reason-react"], 7 | "bs-dev-dependencies": [ 8 | "@glennsl/bs-jest", 9 | "bs-jest-dom", 10 | "bs-react-testing-library", 11 | "bs-webapi" 12 | ], 13 | "sources": [ 14 | { 15 | "dir": "src", 16 | "subdirs": [ 17 | { 18 | "dir": "__tests__", 19 | "type": "dev" 20 | }, 21 | { 22 | "dir": "components", 23 | "subdirs": [ 24 | { 25 | "dir": "__tests__", 26 | "type": "dev" 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | ], 33 | "package-specs": { 34 | "module": "commonjs", 35 | "in-source": true 36 | }, 37 | "suffix": ".bs.js", 38 | "refmt": 3 39 | } 40 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ## Change Log 2 | 3 | ### [v2.0.0](https://github.com/wyze/reason-calculator/releases/tag/v2.0.0) (2019-04-24) 4 | 5 | * Add back version scripts ([@wyze](https://github.com/wyze) in [a370a37](https://github.com/wyze/reason-calculator/commit/a370a37)) 6 | * Update demo URL ([@wyze](https://github.com/wyze) in [35baf4a](https://github.com/wyze/reason-calculator/commit/35baf4a)) 7 | * Meta file updates ([@wyze](https://github.com/wyze) in [ae86464](https://github.com/wyze/reason-calculator/commit/ae86464)) 8 | * Add tests with `bs-react-testing-library` ([@wyze](https://github.com/wyze) in [18da5d2](https://github.com/wyze/reason-calculator/commit/18da5d2)) 9 | * Convert project to reason-react v0.7.0 and JSX 3 syntax ([@wyze](https://github.com/wyze) in [5a44d15](https://github.com/wyze/reason-calculator/commit/5a44d15)) 10 | * Fix build to work on Travis CI ([@wyze](https://github.com/wyze) in [fb3331f](https://github.com/wyze/reason-calculator/commit/fb3331f)) 11 | 12 | ### [v1.4.0](https://github.com/wyze/reason-calculator/releases/tag/v1.4.0) (2017-11-13) 13 | 14 | * [[`714f05e80b`](https://github.com/wyze/reason-calculator/commit/714f05e80b)] - Upgrade dependencies and to Reason 3 syntax (Neil Kistner) 15 | * [[`ead5c1f94e`](https://github.com/wyze/reason-calculator/commit/ead5c1f94e)] - Override user-agent border radius on buttons (Neil Kistner) 16 | * [[`1b04dc0f29`](https://github.com/wyze/reason-calculator/commit/1b04dc0f29)] - Upgrade dependencies (Neil Kistner) 17 | 18 | ### [v1.3.0](https://github.com/wyze/reason-calculator/releases/tag/v1.3.0) (2017-04-15) 19 | 20 | * [[`7348e83f13`](https://github.com/wyze/reason-calculator/commit/7348e83f13)] - Add percentage button (Neil Kistner) 21 | * [[`fa757efb4f`](https://github.com/wyze/reason-calculator/commit/fa757efb4f)] - Remove ClearButton and replace with GreenButton (Neil Kistner) 22 | * [[`1c79f2bbd2`](https://github.com/wyze/reason-calculator/commit/1c79f2bbd2)] - Use stable reductive & bs-jest (#3) (Cheng Lou) 23 | * [[`d391100a17`](https://github.com/wyze/reason-calculator/commit/d391100a17)] - Fix dangereouslySetInnerHTML type (#1) (Cheng Lou) 24 | * [[`dad25d3463`](https://github.com/wyze/reason-calculator/commit/dad25d3463)] - Remove unused dep (#2) (Cheng Lou) 25 | * [[`5d1da1539b`](https://github.com/wyze/reason-calculator/commit/5d1da1539b)] - Cleanup testing process (Neil Kistner) 26 | * [[`233257333f`](https://github.com/wyze/reason-calculator/commit/233257333f)] - Upgrade dependencies and fix build (Neil Kistner) 27 | * [[`ca8eef1903`](https://github.com/wyze/reason-calculator/commit/ca8eef1903)] - Refactor tests (Neil Kistner) 28 | 29 | ### [v1.2.0](https://github.com/wyze/reason-calculator/releases/tag/v1.2.0) (2017-04-06) 30 | 31 | * [[`cd9c2285f1`](https://github.com/wyze/reason-calculator/commit/cd9c2285f1)] - Add ability to enter decimals (Neil Kistner) 32 | * [[`93c692abc1`](https://github.com/wyze/reason-calculator/commit/93c692abc1)] - Remove stale Pending action (Neil Kistner) 33 | * [[`7458b780c5`](https://github.com/wyze/reason-calculator/commit/7458b780c5)] - Convert int to float to prepare for decimal functionality (Neil Kistner) 34 | 35 | ### [v1.1.1](https://github.com/wyze/reason-calculator/releases/tag/v1.1.1) (2017-04-05) 36 | 37 | * [[`1bb0ab69b1`](https://github.com/wyze/reason-calculator/commit/1bb0ab69b1)] - Upgrade reductive (Neil Kistner) 38 | * [[`122df904f3`](https://github.com/wyze/reason-calculator/commit/122df904f3)] - Update styles for a better mobile experience (Neil Kistner) 39 | 40 | ### [v1.1.0](https://github.com/wyze/reason-calculator/releases/tag/v1.1.0) (2017-04-04) 41 | 42 | * [[`872823b937`](https://github.com/wyze/reason-calculator/commit/872823b937)] - Cleanup PosNeg handling and fix potential bug when total would be 0 (Neil Kistner) 43 | * [[`7ce73e39fe`](https://github.com/wyze/reason-calculator/commit/7ce73e39fe)] - Add positive/negative button (Neil Kistner) 44 | * [[`164e1d9cf1`](https://github.com/wyze/reason-calculator/commit/164e1d9cf1)] - Remove all the reversing of the operations (Neil Kistner) 45 | 46 | ### [v1.0.1](https://github.com/wyze/reason-calculator/releases/tag/v1.0.1) (2017-04-03) 47 | 48 | * [[`7db4e02f0f`](https://github.com/wyze/reason-calculator/commit/7db4e02f0f)] - Fix bug when changing operation (i.e. Add -\> Subtract) (Neil Kistner) 49 | * [[`30d699670c`](https://github.com/wyze/reason-calculator/commit/30d699670c)] - Quick style update (Neil Kistner) 50 | * [[`eea0ba9618`](https://github.com/wyze/reason-calculator/commit/eea0ba9618)] - Fix bug with Input action after Equals action (Neil Kistner) 51 | * [[`d08ec46285`](https://github.com/wyze/reason-calculator/commit/d08ec46285)] - General project cleanup (Neil Kistner) 52 | * [[`d79a2512c6`](https://github.com/wyze/reason-calculator/commit/d79a2512c6)] - Add tests with `bs-jest` (Neil Kistner) 53 | * [[`1b1634d0f6`](https://github.com/wyze/reason-calculator/commit/1b1634d0f6)] - Add Svg component (Neil Kistner) 54 | 55 | ### [v1.0.0](https://github.com/wyze/reason-calculator/releases/tag/v1.0.0) (2017-03-30) 56 | 57 | * [[`32e8e5626d`](https://github.com/wyze/reason-calculator/commit/32e8e5626d)] - Initial commit (Neil Kistner) 58 | 59 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Neil Kistner (neilkistner.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "alias": "reason-calculator", 3 | "builds": [{ "src": "package.json", "use": "@now/static-build" }], 4 | "version": 2 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reason-calculator", 3 | "private": true, 4 | "version": "2.0.0", 5 | "repository": "wyze/reason-calculator", 6 | "author": { 7 | "name": "Neil Kistner", 8 | "email": "neil.kistner@gmail.com", 9 | "url": "neilkistner.com" 10 | }, 11 | "license": "MIT", 12 | "scripts": { 13 | "build": "run-s build:*", 14 | "build:bsb": "bsb -make-world", 15 | "build:parcel": "parcel build public/index.html", 16 | "clean": "run-p clean:*", 17 | "clean:bsb": "bsb -clean-world", 18 | "clean:parcel": "rimraf dist lib .merlin", 19 | "postversion": "github-release", 20 | "pretest": "bsb -make-world", 21 | "now-build": "yarn build", 22 | "start": "run-p start:*", 23 | "start:bsb": "bsb -make-world -w", 24 | "start:parcel": "parcel public/index.html", 25 | "test": "yarn jest", 26 | "version": "changelog" 27 | }, 28 | "dependencies": { 29 | "bs-css": "^8.0.4", 30 | "react": "^16.8.6", 31 | "react-dom": "^16.8.6", 32 | "reason-react": "^0.7.0" 33 | }, 34 | "devDependencies": { 35 | "@glennsl/bs-jest": "^0.4.8", 36 | "@wyze/changelog": "^1.0.0", 37 | "@wyze/github-release": "^1.0.0", 38 | "bs-jest-dom": "^2.0.1", 39 | "bs-platform": "^5.0.3", 40 | "bs-react-testing-library": "^0.5.0", 41 | "bs-webapi": "^0.14.2", 42 | "bsb-js": "^1.1.7", 43 | "npm-run-all": "^4.1.5", 44 | "parcel-bundler": "^1.12.3", 45 | "rimraf": "^2.6.3" 46 | }, 47 | "jest": { 48 | "roots": [ 49 | "src" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Calculator in Reason 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # reason-calculator · [![Website][website-image]][website-url] [![Build Status][circleci-image]][circleci-url] 2 | 3 | > A calculator built with [Reason](//github.com/facebook/reason) and [reason-react](//github.com/reasonml/reason-react). 4 | 5 | ## Live Demo 6 | 7 | https://reason-calculator.now.sh 8 | 9 | ## Setup 10 | 11 | ```sh 12 | $ git clone https://github.com/wyze/reason-calculator.git 13 | $ cd reason-calculator 14 | $ yarn # or `npm install` 15 | ``` 16 | 17 | ## Development 18 | 19 | ```sh 20 | $ npm run dev 21 | ``` 22 | 23 | ## Build 24 | 25 | ```sh 26 | $ npm run build 27 | ``` 28 | 29 | ## Change Log 30 | 31 | > [Full Change Log](changelog.md) 32 | 33 | ### [v2.0.0](https://github.com/wyze/reason-calculator/releases/tag/v2.0.0) (2019-04-24) 34 | 35 | * Add back version scripts ([@wyze](https://github.com/wyze) in [a370a37](https://github.com/wyze/reason-calculator/commit/a370a37)) 36 | * Update demo URL ([@wyze](https://github.com/wyze) in [35baf4a](https://github.com/wyze/reason-calculator/commit/35baf4a)) 37 | * Meta file updates ([@wyze](https://github.com/wyze) in [ae86464](https://github.com/wyze/reason-calculator/commit/ae86464)) 38 | * Add tests with `bs-react-testing-library` ([@wyze](https://github.com/wyze) in [18da5d2](https://github.com/wyze/reason-calculator/commit/18da5d2)) 39 | * Convert project to reason-react v0.7.0 and JSX 3 syntax ([@wyze](https://github.com/wyze) in [5a44d15](https://github.com/wyze/reason-calculator/commit/5a44d15)) 40 | * Fix build to work on Travis CI ([@wyze](https://github.com/wyze) in [fb3331f](https://github.com/wyze/reason-calculator/commit/fb3331f)) 41 | 42 | ## License 43 | 44 | MIT © [Neil Kistner](https://neilkistner.com) 45 | 46 | [circleci-image]: https://img.shields.io/circleci/project/github/wyze/reason-calculator.svg?style=flat-square 47 | [circleci-url]: https://circleci.com/gh/wyze/reason-calculator 48 | 49 | [website-image]: https://img.shields.io/website-up-down-green-red/https/reason-calculator.now.sh.svg?style=flat-square 50 | [website-url]: https://reason-calculator.now.sh 51 | -------------------------------------------------------------------------------- /src/__tests__/store_test.re: -------------------------------------------------------------------------------- 1 | open Jest; 2 | open Expect; 3 | open Store; 4 | 5 | let run = actions => 6 | actions 7 | |> List.fold_left((state, action) => reducer(state, action), []) 8 | |> List.map(({action, left, right, total}) => 9 | (left, right, action, total) 10 | ); 11 | 12 | test("reducer handles Clear action", () => { 13 | let actual = [Input("1"), Add, Input("3"), Clear]->run; 14 | let expected = []; 15 | 16 | expect(actual) |> toEqual(expected); 17 | }); 18 | 19 | test("reducer handles Equals action", () => { 20 | let actual = [Input("1"), Add, Input("2"), Equals]->run; 21 | let expected = [("3", "3", Equals, 3.), ("1", "2", Add, 3.)]; 22 | 23 | expect(actual) |> toEqual(expected); 24 | }); 25 | 26 | test("reducer handles double Equals action", () => { 27 | let actual = [Input("1"), Add, Input("2"), Equals, Equals]->run; 28 | let expected = [ 29 | ("5", "5", Equals, 5.), 30 | ("3", "2", Add, 5.), 31 | ("3", "3", Equals, 3.), 32 | ("1", "2", Add, 3.), 33 | ]; 34 | 35 | expect(actual) |> toEqual(expected); 36 | }); 37 | 38 | test("reducer handles Input action with no previous state", () => { 39 | let actual = [Input("4")]->run; 40 | let expected = [("4", "", Pending, 4.)]; 41 | 42 | expect(actual) |> toEqual(expected); 43 | }); 44 | 45 | test("reducer handles Input action with Pending state", () => { 46 | let actual = [Input("1"), Input("4")]->run; 47 | let expected = [("14", "", Pending, 14.)]; 48 | 49 | expect(actual) |> toEqual(expected); 50 | }); 51 | 52 | test("reducer handles Input action with non-Pending state", () => { 53 | let actual = [Input("1"), Add, Input("4")]->run; 54 | let expected = [("1", "4", Add, 5.)]; 55 | 56 | expect(actual) |> toEqual(expected); 57 | }); 58 | 59 | test("reducer handles Input action after Equals state", () => { 60 | let actual = [Input("1"), Add, Input("2"), Equals, Input("4")]->run; 61 | let expected = [ 62 | ("4", "", Pending, 4.), 63 | ("3", "3", Equals, 3.), 64 | ("1", "2", Add, 3.), 65 | ]; 66 | 67 | expect(actual) |> toEqual(expected); 68 | }); 69 | 70 | test("reducer handles Add action", () => { 71 | let actual = [Input("1"), Add]->run; 72 | let expected = [("1", "", Add, 1.)]; 73 | 74 | expect(actual) |> toEqual(expected); 75 | }); 76 | 77 | test("reducer handles switch from Add to Subtract action", () => { 78 | let actual = [Input("1"), Add, Subtract]->run; 79 | let expected = [("1", "", Subtract, 1.)]; 80 | 81 | expect(actual) |> toEqual(expected); 82 | }); 83 | 84 | test("reducer handles Add action after Equals action", () => { 85 | let actual = [Input("1"), Add, Input("1"), Equals, Add]->run; 86 | let expected = [ 87 | ("2", "", Add, 2.), 88 | ("2", "2", Equals, 2.), 89 | ("1", "1", Add, 2.), 90 | ]; 91 | 92 | expect(actual) |> toEqual(expected); 93 | }); 94 | 95 | describe("PosNeg", () => { 96 | test("with initial state", () => { 97 | let actual = [PosNeg]->run; 98 | let expected = []; 99 | 100 | expect(actual) |> toEqual(expected); 101 | }); 102 | 103 | test("with Pending state", () => { 104 | let actual = [Input("4"), PosNeg]->run; 105 | let expected = [("-4", "", Pending, (-4.))]; 106 | 107 | expect(actual) |> toEqual(expected); 108 | }); 109 | 110 | test("when total would be 0", () => { 111 | let actual = [Input("5"), Subtract, Input("5"), PosNeg]->run; 112 | let expected = [("5", "-5", Subtract, 10.)]; 113 | 114 | expect(actual) |> toEqual(expected); 115 | }); 116 | 117 | test("with no right value", () => { 118 | let actual = [Input("4"), Add, PosNeg]->run; 119 | let expected = [("4", "-4", Add, 0.)]; 120 | 121 | expect(actual) |> toEqual(expected); 122 | }); 123 | 124 | test("with non-Pending state", () => { 125 | let actual = [Input("4"), Add, Input("2"), PosNeg]->run; 126 | let expected = [("4", "-2", Add, 2.)]; 127 | 128 | expect(actual) |> toEqual(expected); 129 | }); 130 | 131 | test("handles sequence: PosNeg -> Equals -> Equals", () => { 132 | let actual = 133 | [Input("5"), Multiply, Input("3"), PosNeg, Equals, Equals]->run; 134 | let expected = [ 135 | ("45", "45", Equals, 45.), 136 | ("-15", "-3", Multiply, 45.), 137 | ("-15", "-15", Equals, (-15.)), 138 | ("5", "-3", Multiply, (-15.)), 139 | ]; 140 | 141 | expect(actual) |> toEqual(expected); 142 | }); 143 | 144 | test("handles sequence: Equals -> PosNeg -> Equals", () => { 145 | let actual = 146 | [Input("5"), Multiply, Input("3"), PosNeg, Equals, PosNeg, Equals] 147 | ->run; 148 | let expected = [ 149 | ("-45", "-45", Equals, (-45.)), 150 | ("15", "-3", Multiply, (-45.)), 151 | ("15", "15", Equals, 15.), 152 | ("5", "-3", Multiply, (-15.)), 153 | ]; 154 | 155 | expect(actual) |> toEqual(expected); 156 | }); 157 | }); 158 | 159 | test("reducer handles decimals", () => { 160 | let actual = 161 | [ 162 | Input("5"), 163 | Input("."), 164 | Input("3"), 165 | Add, 166 | Input("4"), 167 | Input("."), 168 | Input("7"), 169 | Equals, 170 | ] 171 | ->run; 172 | let expected = [("10", "10", Equals, 10.), ("5.3", "4.7", Add, 10.)]; 173 | 174 | expect(actual) |> toEqual(expected); 175 | }); 176 | 177 | test("reducer handles double decimals", () => { 178 | let actual = 179 | [ 180 | Input("2"), 181 | Input("."), 182 | Input("."), 183 | Input("3"), 184 | Add, 185 | Input("2"), 186 | Input("."), 187 | Input("7"), 188 | Input("."), 189 | Input("5"), 190 | Equals, 191 | ] 192 | ->run; 193 | let expected = [ 194 | ("5.05", "5.05", Equals, 5.05), 195 | ("2.3", "2.75", Add, 5.05), 196 | ]; 197 | 198 | expect(actual) |> toEqual(expected); 199 | }); 200 | 201 | describe("Percent", () => { 202 | test("with initial state", () => { 203 | let actual = [Percent]->run; 204 | let expected = []; 205 | 206 | expect(actual) |> toEqual(expected); 207 | }); 208 | 209 | test("with Pending state", () => { 210 | let actual = [Input("4"), Percent]->run; 211 | let expected = [("0.04", "", Pending, 0.04)]; 212 | 213 | expect(actual) |> toEqual(expected); 214 | }); 215 | 216 | test("with no right value", () => { 217 | let actual = [Input("4"), Add, Percent]->run; 218 | let expected = [("4", "0.04", Add, 4.04)]; 219 | 220 | expect(actual) |> toEqual(expected); 221 | }); 222 | 223 | test("with non-Pending state", () => { 224 | let actual = [Input("4"), Add, Input("2"), Percent]->run; 225 | let expected = [("4", "0.02", Add, 4.02)]; 226 | 227 | expect(actual) |> toEqual(expected); 228 | }); 229 | 230 | test("handles sequence: Percent -> Equals -> Equals", () => { 231 | let actual = 232 | [ 233 | Input("5"), 234 | Multiply, 235 | Input("5"), 236 | Input("0"), 237 | Percent, 238 | Equals, 239 | Equals, 240 | ] 241 | ->run; 242 | let expected = [ 243 | ("1.25", "1.25", Equals, 1.25), 244 | ("2.5", "0.5", Multiply, 1.25), 245 | ("2.5", "2.5", Equals, 2.5), 246 | ("5", "0.5", Multiply, 2.5), 247 | ]; 248 | 249 | expect(actual) |> toEqual(expected); 250 | }); 251 | 252 | test("handles sequence: Equals -> Percent -> Equals", () => { 253 | let actual = 254 | [ 255 | Input("5"), 256 | Multiply, 257 | Input("5"), 258 | Input("0"), 259 | Percent, 260 | Equals, 261 | Percent, 262 | Equals, 263 | ] 264 | ->run; 265 | let expected = [ 266 | ("0.0125", "0.0125", Equals, 0.0125), 267 | ("0.025", "0.5", Multiply, 0.0125), 268 | ("0.025", "0.025", Equals, 0.025), 269 | ("5", "0.5", Multiply, 2.5), 270 | ]; 271 | 272 | expect(actual) |> toEqual(expected); 273 | }); 274 | }); 275 | -------------------------------------------------------------------------------- /src/components/App.re: -------------------------------------------------------------------------------- 1 | module Styles = { 2 | open Css; 3 | 4 | let buttons = 5 | style([ 6 | display(`flex), 7 | selector( 8 | "&:last-of-type", 9 | [ 10 | selector( 11 | "& :first-of-type", 12 | [ 13 | flexBasis(pct(50.)), 14 | paddingLeft(em(0.9)), 15 | textAlign(`left), 16 | ], 17 | ), 18 | ], 19 | ), 20 | ]); 21 | 22 | let calculator = 23 | style([ 24 | background(hex("838383")), 25 | borderRadius(px(5)), 26 | overflow(hidden), 27 | width(em(14.)), 28 | ]); 29 | 30 | let container = 31 | style([alignItems(`center), display(`flex), flexDirection(`column)]); 32 | 33 | let display = 34 | style([ 35 | alignItems(center), 36 | color(hex("fafafa")), 37 | display(`flex), 38 | fontSize(em(1.5)), 39 | height(em(2.5)), 40 | justifyContent(flexEnd), 41 | padding2(~v=zero, ~h=em(1.)), 42 | ]); 43 | }; 44 | 45 | [@react.component] 46 | let make = () => { 47 | let display = Store.useDisplay(); 48 | 49 |
50 | 51 |
52 | 56 |
57 |
62 |
63 |
68 |
69 |
74 |
75 |
80 |
81 |
85 |
86 |
; 87 | }; 88 | -------------------------------------------------------------------------------- /src/components/Button.re: -------------------------------------------------------------------------------- 1 | module Styles = { 2 | open Css; 3 | 4 | let container = 5 | merge([ 6 | style([ 7 | borderWidth(zero), 8 | borderRadius(zero), 9 | color(hex("fafafa")), 10 | cursor(`pointer), 11 | display(inlineBlock), 12 | flexBasis(pct(25.)), 13 | fontSize(em(1.5)), 14 | lineHeight(`abs(2.)), 15 | outlineStyle(none), 16 | transitions([ 17 | transition( 18 | ~duration=300, 19 | ~timingFunction=easeOut, 20 | "background-color", 21 | ), 22 | transition(~duration=300, ~timingFunction=easeOut, "color"), 23 | ]), 24 | hover([backgroundColor(hex("fafafa"))]), 25 | ]), 26 | ]); 27 | 28 | let variant = colorCode => 29 | style([ 30 | backgroundColor(hex(colorCode)), 31 | hover([color(hex(colorCode))]), 32 | ]); 33 | 34 | let blue = variant("6d71ff"); 35 | let green = variant("3bf3a9"); 36 | let orange = variant("ff8754"); 37 | }; 38 | 39 | [@react.component] 40 | let make = (~action: Store.action, ~text) => { 41 | let color = 42 | switch (action) { 43 | | Clear 44 | | Percent 45 | | PosNeg => Styles.green 46 | | Add 47 | | Divide 48 | | Equals 49 | | Multiply 50 | | Subtract => Styles.orange 51 | | _ => Styles.blue 52 | }; 53 | 54 | let addOperation = Store.useAddOperation(); 55 | let onClick = React.useCallback(_ => addOperation(action)); 56 | 57 | ; 60 | }; 61 | -------------------------------------------------------------------------------- /src/components/Checkmark.re: -------------------------------------------------------------------------------- 1 | [@react.component] 2 | let make = () => 3 | 4 | 5 | 6 | ; 7 | -------------------------------------------------------------------------------- /src/components/Feature.re: -------------------------------------------------------------------------------- 1 | type emoji = 2 | | Checkmark 3 | | Soon; 4 | 5 | module Styles = { 6 | open Css; 7 | 8 | let container = 9 | merge([ 10 | style([ 11 | alignItems(`center), 12 | display(`flex), 13 | flexBasis(pct(40.)), 14 | height(em(2.)), 15 | ]), 16 | style([media("(min-width: 53.125em)", [flexBasis(pct(35.))])]), 17 | ]); 18 | }; 19 | 20 | [@react.component] 21 | let make = (~emoji: emoji, ~text) => 22 |
23 | { 24 | switch (emoji) { 25 | | Checkmark => 26 | | Soon => 27 | } 28 | } 29 |

text->React.string

30 |
; 31 | -------------------------------------------------------------------------------- /src/components/Features.re: -------------------------------------------------------------------------------- 1 | module Styles = { 2 | open Css; 3 | 4 | let container = 5 | style([display(`flex), flexWrap(`wrap), justifyContent(`spaceAround)]); 6 | }; 7 | 8 | [@react.component] 9 | let make = () => 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
; 18 | -------------------------------------------------------------------------------- /src/components/Hero.re: -------------------------------------------------------------------------------- 1 | module Styles = { 2 | open Css; 3 | 4 | let container = 5 | merge([ 6 | style([ 7 | background(hex("dedede")), 8 | borderRadius(px(5)), 9 | display(block), 10 | padding(em(1.)), 11 | marginBottom(em(4.)), 12 | textAlign(center), 13 | width(em(30.)), 14 | ]), 15 | style([media("(min-width: 53.125em)", [width(em(35.))])]), 16 | ]); 17 | 18 | let title = style([fontSize(em(1.5))]); 19 | }; 20 | 21 | [@react.component] 22 | let make = () => 23 |
24 |

"Reason Calculator"->React.string

25 | 26 |
; 27 | -------------------------------------------------------------------------------- /src/components/Root.re: -------------------------------------------------------------------------------- 1 | [@react.component] 2 | let make = () => { 3 | let value = React.useReducer(Store.reducer, []); 4 | 5 | ; 6 | }; 7 | -------------------------------------------------------------------------------- /src/components/Soon.re: -------------------------------------------------------------------------------- 1 | [@react.component] 2 | let make = () => 3 | 4 | 5 | 6 | 9 | 12 | 15 | 16 | 17 | ; 18 | -------------------------------------------------------------------------------- /src/components/Svg.re: -------------------------------------------------------------------------------- 1 | module Styles = { 2 | open Css; 3 | 4 | let container = 5 | style([ 6 | height(px(16)), 7 | marginRight(em(1.)), 8 | marginTop(`zero), 9 | width(px(16)), 10 | ]); 11 | }; 12 | 13 | [@react.component] 14 | let make = (~children, ~title) => 15 | 19 | title->React.string 20 | children 21 | ; 22 | -------------------------------------------------------------------------------- /src/components/__tests__/App_test.re: -------------------------------------------------------------------------------- 1 | open Jest; 2 | open Expect; 3 | open JestDom; 4 | open ReactTestingLibrary; 5 | open Webapi.Dom; 6 | 7 | let unwrap = element => 8 | switch (element) { 9 | | Some(el) => el 10 | | None => raise(Failure("Element not found")) 11 | }; 12 | 13 | test("renders the title", () => 14 | 15 | |> render 16 | |> getByText(~matcher=`Str("Reason Calculator")) 17 | |> expect 18 | |> toBeInTheDocument 19 | ); 20 | 21 | testAll( 22 | "renders feature", 23 | [ 24 | ("Simple operations", "checkmark"), 25 | ("Decimals", "checkmark"), 26 | ("Percentage", "checkmark"), 27 | ("Positive/Negative", "checkmark"), 28 | ("Advanced options", "soon"), 29 | ("Operation history", "soon"), 30 | ], 31 | ((feature, icon)) => 32 | 33 | |> render 34 | |> getByText( 35 | ~matcher= 36 | `Func( 37 | (_, el) => 38 | el->Element.parentNode->unwrap->Node.textContent == icon 39 | ++ feature, 40 | ), 41 | ) 42 | |> expect 43 | |> toBeInTheDocument 44 | ); 45 | 46 | test("renders display", () => { 47 | let app = |> render; 48 | let oneButton = app |> getByText(~matcher=`Str("1")); 49 | 50 | oneButton->FireEvent.click; 51 | oneButton->FireEvent.click; 52 | 53 | app |> getByLabelText(~matcher=`Str("11")) |> expect |> toBeInTheDocument; 54 | }); 55 | 56 | testAll( 57 | "performs operation", 58 | [ 59 | (["1", "+", "2", "="], "3"), 60 | (["1", {js|\u2212|js}, "2", "="], "-1"), 61 | (["2", {js|\u00d7|js}, "3", "="], "6"), 62 | (["4", {js|\u00f7|js}, "2", "="], "2"), 63 | ], 64 | ((operations, result)) => { 65 | let app = |> render; 66 | 67 | List.iter( 68 | operation => 69 | app |> getByText(~matcher=`Str(operation)) |> FireEvent.click, 70 | operations, 71 | ); 72 | 73 | app 74 | |> getByLabelText(~matcher=`Str(result)) 75 | |> expect 76 | |> toBeInTheDocument; 77 | }, 78 | ); 79 | -------------------------------------------------------------------------------- /src/globalStyles.re: -------------------------------------------------------------------------------- 1 | open Css; 2 | 3 | /* Resets */ 4 | global("html, body, h1, h2, h3", [margin(`zero), padding(`zero)]); 5 | global("h1, h2, h3", [fontSize(pct(100.)), fontWeight(`normal)]); 6 | global("button", [margin(`zero)]); 7 | 8 | /* Box sizing */ 9 | global("html", [boxSizing(`inherit_)]); 10 | global("*, *:after, *:before", [boxSizing(`borderBox)]); 11 | 12 | /* Global styles */ 13 | global( 14 | ":root", 15 | [ 16 | fontSize(em(0.75)), 17 | media( 18 | "(min-width: 25em) and (max-width: 93.75em)", 19 | [ 20 | /* Using unsafe here because too much math */ 21 | unsafe( 22 | "fontSize", 23 | "calc(.75em + (18 - 12) * ((100vw - 25em) / (1500 - 400)))", 24 | ), 25 | ], 26 | ), 27 | media("(min-width: 93.75em)", [fontSize(em(1.125))]), 28 | ], 29 | ); 30 | global( 31 | "html", 32 | [ 33 | lineHeight(`abs(1.45)), 34 | unsafe("MozOsxFontSmoothing", "grayscale"), 35 | unsafe("WebkitFontSmoothing", "antialiased"), 36 | overflowX(hidden), 37 | unsafe("textRendering", "optimizeLegibility"), 38 | ], 39 | ); 40 | global( 41 | "body", 42 | [ 43 | background(hex("efefef")), 44 | color(hex("838383")), 45 | display(`flex), 46 | fontFamily( 47 | [| 48 | "-apple-system", 49 | "BlinkMacSystemFont", 50 | "'Segoe UI'", 51 | "Roboto", 52 | "Helvetica", 53 | "Arial", 54 | "sans-serif", 55 | "'Apple Color Emoji'", 56 | "'Segoe UI Emoji'", 57 | "'Segoe UI Symbol'", 58 | |] 59 | |> Js.Array.joinWith(", "), 60 | ), 61 | height(`calc((`sub, vh(100.), em(3.)))), 62 | justifyContent(`center), 63 | marginTop(em(3.)), 64 | ], 65 | ); 66 | -------------------------------------------------------------------------------- /src/index.re: -------------------------------------------------------------------------------- 1 | include GlobalStyles; 2 | 3 | ReactDOMRe.renderToElementWithId(, "root"); 4 | -------------------------------------------------------------------------------- /src/store.re: -------------------------------------------------------------------------------- 1 | type action = 2 | | Add 3 | | Clear 4 | | Divide 5 | | Equals 6 | | Pending 7 | | Input(string) 8 | | Multiply 9 | | Subtract 10 | | PosNeg 11 | | Percent; 12 | 13 | type operation = { 14 | action, 15 | left: string, 16 | right: string, 17 | total: float, 18 | }; 19 | 20 | type state = list(operation); 21 | 22 | module Operation = { 23 | let default = {left: "", right: "", action: Pending, total: 0.}; 24 | 25 | let make = () => default; 26 | 27 | let toInfix = action => 28 | switch (action) { 29 | | Add => (+.) 30 | | Divide => (/.) 31 | | Multiply => ( *. ) 32 | | Subtract => (-.) 33 | /* noop function */ 34 | | _ => ((left, _) => left) 35 | }; 36 | 37 | let strEmpty = str => String.length(str) == 0; 38 | let isFloat = str => Js.String.includes(".", str); 39 | let toFloat = str => strEmpty(str) ? 0. : float_of_string(str); 40 | let toString = flt => { 41 | let str = Js.Float.toString(flt); 42 | 43 | Js.String.endsWith(".", str) ? Js.String.replace(".", "", str) : str; 44 | }; 45 | 46 | let find = (lst, predicate) => 47 | switch (lst) { 48 | | [] => default 49 | | _ => lst |> List.find(predicate) 50 | }; 51 | 52 | let multiply = (factor, num) => 53 | num |> toFloat |> ( *. )(factor) |> toString; 54 | 55 | let execute = (infix, left, right) => 56 | infix(toFloat(left), toFloat(right)); 57 | 58 | let update = (input, {left, right, action}) => { 59 | let (left', right') = 60 | switch (input, action) { 61 | | (".", Pending) => (isFloat(left) ? left : left ++ input, right) 62 | | (".", _) => (left, isFloat(right) ? right : right ++ input) 63 | | (_, Pending) => (left ++ input, right) 64 | | (_, Equals) => (left, right) 65 | | (_, _) => (left, right ++ input) 66 | }; 67 | 68 | { 69 | left: left', 70 | right: right', 71 | action, 72 | total: action->toInfix->execute(left', right'), 73 | }; 74 | }; 75 | 76 | let createEquals = ({total}) => { 77 | let sTotal = toString(total); 78 | 79 | {left: sTotal, right: sTotal, action: Equals, total}; 80 | }; 81 | 82 | let addEquals = (lst, cur) => { 83 | let {right, action} = find(lst, ({action}) => action != Equals); 84 | let prev = 85 | update("", {left: cur.total->toString, right, action, total: 0.}); 86 | 87 | switch (cur.action) { 88 | | Equals => [createEquals(prev), prev] 89 | | _ => [createEquals(cur)] 90 | }; 91 | }; 92 | 93 | let doOperation = (factor, {left, right, action}) => { 94 | let op = multiply(factor); 95 | let (left', right') = 96 | switch (right, action) { 97 | | ("", Pending) => (op(left), right) 98 | | ("", _) => (left, op(left)) 99 | | (_, Equals) => (op(left), op(right)) 100 | | (_, _) => (left, op(right)) 101 | }; 102 | 103 | update("", {left: left', right: right', action, total: 0.}); 104 | }; 105 | }; 106 | 107 | let context: React.Context.t((state, action => unit)) = React.createContext(([], _ => ())); 108 | 109 | module Provider = { 110 | let make = context->React.Context.provider; 111 | 112 | [@bs.obj] 113 | external makeProps: 114 | (~value: 'a, ~children: React.element, ~key: string=?, unit) => 115 | { 116 | . 117 | "value": 'a, 118 | "children": React.element, 119 | } = 120 | ""; 121 | }; 122 | 123 | let reducer = (state, action) => { 124 | let ({total} as current, old) = 125 | switch (state) { 126 | | [hd, ...tl] => (hd, tl) 127 | | [] => (Operation.default, state) 128 | }; 129 | 130 | switch (action) { 131 | | Clear => [] 132 | | Equals => Operation.addEquals(state, current) @ state 133 | | Input(input) => 134 | switch (current.action) { 135 | | Equals => [Operation.default |> Operation.update(input)] @ state 136 | | _ => [current |> Operation.update(input)] @ old 137 | } 138 | | Pending => state 139 | | Percent 140 | | PosNeg => 141 | switch (action, current.action, total) { 142 | | (_, Pending, 0.) => state 143 | | (Percent, _, _) => [Operation.doOperation(0.01, current)] @ old 144 | | (PosNeg, _, _) => [Operation.doOperation(-1., current)] @ old 145 | | (_, _, _) => state /* Prevent warning of possible value */ 146 | } 147 | | _ => [ 148 | {left: total->Operation.toString, right: "", action, total}, 149 | ...switch (current.action) { 150 | | Equals => state 151 | | _ => old 152 | }, 153 | ] 154 | }; 155 | }; 156 | 157 | let useDisplay = () => { 158 | let (operations, _) = React.useContext(context); 159 | 160 | switch (operations) { 161 | | [] => "0" 162 | | ops => 163 | let {left, right} = List.hd(ops); 164 | 165 | Operation.strEmpty(right) ? left : right; 166 | }; 167 | }; 168 | 169 | let useAddOperation = () => { 170 | let (_, dispatch) = React.useContext(context); 171 | 172 | action => action->dispatch; 173 | }; 174 | --------------------------------------------------------------------------------