├── .eslintrc ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── clone.png ├── fork.png ├── linebreak-error.png ├── pr.png ├── pr2.png ├── status.png └── vsc-endline.png ├── .gitignore ├── .prettierrc ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── babel.config.js ├── logo.png ├── package-lock.json ├── package.json ├── src ├── css │ └── main.css ├── favicon.ico ├── index.html └── js │ ├── app.js │ ├── components │ ├── App.js │ ├── ChangeButton.js │ ├── Char.js │ ├── GithubCorner.js │ ├── Hint.js │ ├── Logo.js │ ├── ProgressBar.js │ ├── Rebus.js │ └── Word.js │ ├── mini │ ├── component.js │ ├── index.js │ ├── render.js │ ├── store.js │ └── utils.js │ ├── rebuses.js │ └── store.js ├── tests ├── __mocks__ │ └── styleMock.js ├── __snapshots__ │ ├── app.spec.js.snap │ ├── components.spec.js.snap │ ├── mini.spec.js.snap │ └── store.spec.js.snap ├── app.spec.js ├── components.spec.js ├── mini.spec.js ├── rebus.spec.js └── store.spec.js └── webpack.config.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["airbnb", "prettier"], 4 | "plugins": ["react", "jsx-a11y", "import", "prettier"], 5 | "env": { 6 | "jest": true, 7 | "browser": true 8 | }, 9 | "rules": { 10 | "prettier/prettier": ["error"], 11 | "no-use-before-define": 0, 12 | "lines-between-class-members": 0, 13 | "import/prefer-default-export": 0 14 | }, 15 | "globals": { 16 | "document": true, 17 | "Event": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | This project adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct). 4 | Please read the full text so that you can understand what actions will and will not be tolerated. 5 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to rebus 2 | 3 | For a step-by-step guide on how to make your first contribution, have a look in the [README](https://github.com/ollelauribostrom/rebus#rebus). 4 | 5 | ## Contribution process overview 6 | 7 | 1. Fork this project. 8 | 1. Create a feature branch. 9 | 1. Make your changes. 10 | 1. Run the game locally 11 | 1. Run the tests. 12 | 1. Push your changes to your fork/branch. 13 | 1. Open a pull request. 14 | 15 | ### 1. Fork 16 | 17 | 1. Click the fork button up top. 18 | 1. Clone your fork locally (Notice that git's `origin` reference will point to your forked repository). 19 | 1. It is useful to have the upstream repository registered as well using: `git remote add upstream https://github.com/ollelauribostrom/rebus.git` and periodically fetch it using `git fetch upstream`. 20 | 21 | ### 2. Create a feature branch 22 | 23 | Create and switch to a new feature branch: `git checkout -b {branch_name} upstream/master` 24 | (replace `{branch_name}` with a meaningful name that describes your feature or change). 25 | 26 | ### 3. Make your changes 27 | 28 | Now that you have a new branch you can edit/create/delete files. Use touch-up commits using `git commit --amend`. (You may use git force push after that). 29 | 30 | ### 4. Run the game locally 31 | 32 | - Install the dependencies: `npm install`. 33 | - Start the local development server: `npm start`. 34 | 35 | ### 5. Run the tests 36 | 37 | - Run tests: `npm test`. 38 | - Run lint: `npm run lint`. 39 | - Run tests and lint: `npm run test:all`. 40 | 41 | ### 6. Push your changes to your fork/branch 42 | 43 | After lint and all tests pass, push the changes to your fork/branch on GitHub: `git push origin {branch_name}`. For force push, which will destroy previous commits on the server, use `--force` (or `-f`) option. 44 | 45 | ### 7. Create a pull request 46 | 47 | Create a pull request on GitHub for your feature branch. 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | ... 4 | 5 | ## Actual Behavior 6 | 7 | ... 8 | 9 | ## Steps to Reproduce the Problem 10 | 11 | 1. Step 1.. 12 | 2. Step 2.. 13 | 3. Step 3.. 14 | 15 | ## Specifications 16 | 17 | - Browser: 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Associated Issue: # 2 | 3 | ### Summary of Changes 4 | 5 | - change 1 6 | - change 2 7 | -------------------------------------------------------------------------------- /.github/clone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollelauribostrom/rebus/37a09197fbe5ecfb497692ba159f076f55f1136e/.github/clone.png -------------------------------------------------------------------------------- /.github/fork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollelauribostrom/rebus/37a09197fbe5ecfb497692ba159f076f55f1136e/.github/fork.png -------------------------------------------------------------------------------- /.github/linebreak-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollelauribostrom/rebus/37a09197fbe5ecfb497692ba159f076f55f1136e/.github/linebreak-error.png -------------------------------------------------------------------------------- /.github/pr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollelauribostrom/rebus/37a09197fbe5ecfb497692ba159f076f55f1136e/.github/pr.png -------------------------------------------------------------------------------- /.github/pr2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollelauribostrom/rebus/37a09197fbe5ecfb497692ba159f076f55f1136e/.github/pr2.png -------------------------------------------------------------------------------- /.github/status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollelauribostrom/rebus/37a09197fbe5ecfb497692ba159f076f55f1136e/.github/status.png -------------------------------------------------------------------------------- /.github/vsc-endline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollelauribostrom/rebus/37a09197fbe5ecfb497692ba159f076f55f1136e/.github/vsc-endline.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | yarn-error.log 4 | .DS_Store 5 | dist 6 | coverage 7 | .idea 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '10' 5 | branches: 6 | only: 7 | - master 8 | cache: 9 | directories: 10 | - node_modules 11 | notifications: 12 | email: 13 | on_success: never 14 | on_failure: always 15 | webhooks: 16 | urls: 17 | - https://webhooks.gitter.im/e/d6e8722e284fbf3d51b2 18 | on_success: change 19 | on_failure: always 20 | install: 21 | - npm install 22 | script: 23 | - npm run test:all 24 | - npm run coveralls 25 | after_success: 26 | - npm run build 27 | deploy: 28 | local-dir: dist 29 | provider: pages 30 | skip-cleanup: true 31 | github-token: $GITHUB_TOKEN 32 | name: 'Rebus bot' 33 | on: 34 | branch: master 35 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "javascript.format.enable": false 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Olle Lauri Boström 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rebus 2 | 3 | [![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)](https://github.com/ellerbrock/open-source-badges/) 4 | [![Build Status](https://travis-ci.org/ollelauribostrom/rebus.svg?branch=master)](https://travis-ci.org/ollelauribostrom/rebus) 5 | [![Coverage Status](https://coveralls.io/repos/github/ollelauribostrom/rebus/badge.svg?branch=master)](https://coveralls.io/github/ollelauribostrom/rebus?branch=master) 6 | [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/rebus-contributors/Lobby) 7 | [![first-timers-only](https://img.shields.io/badge/first--timers--only-friendly-blue.svg?style=flat-square)](https://www.firsttimersonly.com/) 8 | 9 | Contributing to an open source project for the first time can be a scary thing. The goal of this repo is to help you take your first steps as an open source contributor by developing a simple (but hopefully fun) rebus game together. 💖 10 | 11 | #### Try the live version of the game: https://ollelauribostrom.github.io/rebus/ 12 | 13 | ## Who can contribute? 14 | 15 | Everyone can! (and I mean everyone) 💫 16 | 17 | 💻 **You don’t have to contribute code**. Add a new rebus, fix a typo, report a bug, add some documentation, do some re-design or add a translation. This project just like most open source projects are in need of all sorts of different contributions. Not just code. 18 | 19 | 🌟 **You are good enough**. Start off easy by fixing something small (like adding a new rebus). This will help you orient yourself in the project and increase your confidence and experience. No one will judge you if you make a mistake, and you can't break anything! Ask for some pointers if you get stuck. You got this! 20 | 21 | ➡️ Have a look at the [open issues](https://github.com/ollelauribostrom/rebus/issues) to see what needs to be done in this project. 22 | 23 | The only thing that is required to get started is git. Make sure you have it installed on your computer by running `git --version` in your terminal. If you do not have git installed, [install it](https://help.github.com/articles/set-up-git/). 24 | 25 | If you are an experienced developer, look at the [CONTRIBUTING file](https://github.com/ollelauribostrom/rebus/blob/master/.github/CONTRIBUTING.md) to see how you can contribute. 26 | 27 | ## Why contribute to open source? 28 | 29 | When you contribute to Open Source, you are taking part in the collaborative effort of a vast community of passionate developers and contributors! Open Source software allows you to see, use, and more importantly modify its source code. Contributing to Open Source is a great way to develop a deeper understanding of software, and the best part is being able to learn and teach alongside a community of contributors. 30 | 31 | ## How to contribute 32 | 33 | Follow this step-by-step guide to make your first open source contribution. The steps you will perform in this guide is a somewhat standard workflow that you will encounter in most projects: `Fork -> Clone -> Install dependencies -> Make your changes -> Run tests -> Commit -> PR` 34 | 35 | ### 1. Fork 36 | 37 | The first step is to create a fork of this repo. Do so by clicking on the fork button on the top of this page. A fork is basically your own working copy of this repository. 38 | 39 | Forking the repo 40 | 41 | ### 2. Clone 42 | 43 | The next step is to clone the forked repo to your machine. 44 | 45 | Go to your GitHub repositories and open the forked repository called Rebus (_forked from ollelauribostrom/rebus_). Click on the "Clone or download" button and then click the copy to clipboard icon to get your url. 46 | 47 | Cloning the repo 48 | 49 | Finally run the following git command in your terminal: 50 | 51 | ```sh 52 | git clone "the copied url" 53 | ``` 54 | 55 | For example: 56 | 57 | ``` 58 | git clone https://github.com/username/rebus.git 59 | ``` 60 | 61 | ### 3. Register the upstream repository 62 | 63 | You have now created a local clone on you computer. This clone will point to your forked repository. It's also useful to have 64 | the upstream repository (the source that you forked) registered as well to be able to stay up to date with the latest changes. 65 | 66 | If you haven't already, start by changing your directory to the rebus repository that was created when you ran `git clone`: 67 | 68 | ```sh 69 | cd rebus 70 | ``` 71 | 72 | Then add `ollelauribostrom/rebus` as the upstream remote: 73 | 74 | ``` 75 | git remote add upstream https://github.com/ollelauribostrom/rebus.git 76 | ``` 77 | 78 | ### 4. Create a branch 79 | 80 | It's common practice to create a new branch for each new feature or bugfix you are working on. Let's go ahead and create one! 81 | 82 | First, lets make sure we have the latest version of the upstream repository by running (do this before each time you create a new branch): 83 | 84 | ```sh 85 | git fetch upstream 86 | ``` 87 | 88 | Create your new branch by running: 89 | 90 | ```sh 91 | git checkout -b upstream/master 92 | ``` 93 | 94 | > Note: Replace `` with something that describes the changes you are about to make 95 | 96 | For example: 97 | 98 | ```sh 99 | git checkout -b add-new-rebus upstream/master 100 | ``` 101 | 102 | > Note: By specifying `upstream/master` we're saying that our new branch should be created from the latest upstream version 103 | 104 | ### 5. Install the dependencies 105 | 106 | Before we begin making our changes, let's install the projects dependencies: 107 | 108 | ```sh 109 | npm install 110 | ``` 111 | 112 | ### 6. Make your changes 113 | 114 | Now it's time to make your changes. Let's add a new rebus to the game. 115 | 116 | 1. Open the file `src/js/rebuses.js` in your favourite editor (preferable VSCode 😉). 117 | 1. Add a new rebus object to the end of the `rebuses` array. 118 | 1. Save the file when you are done. 119 | 120 | ### 7. Run the game locally 121 | 122 | If you want, you can run the game locally to try out your changes: 123 | 124 | ```sh 125 | npm start 126 | ``` 127 | 128 | ### 8. Run the tests 129 | 130 | Before your commit your changes, run the tests to make sure you didn't break anything: 131 | 132 | ```sh 133 | npm run test:all 134 | ``` 135 | 136 | ### 9. Commit your changes 137 | 138 | Run `git status` to see which changes you have made. This will look something like: 139 | 140 | Git status 141 | 142 | Add these changes to your next commit by running: 143 | 144 | ```sh 145 | git add src/js/rebuses.js 146 | ``` 147 | 148 | And then commit them by running: 149 | 150 | ```sh 151 | git commit -m "Your message" 152 | ``` 153 | 154 | For example: 155 | 156 | ```sh 157 | git commit -m "Adding a new rebus" 158 | ``` 159 | 160 | ### 10. Push your changes to Github 161 | 162 | Push your changes to GitHub by running: 163 | 164 | ```sh 165 | git push origin 166 | ``` 167 | 168 | > Note: Replace `` with the name of your branch 169 | 170 | ### 11. Open a Pull Request 171 | 172 | Head over to your repository on GitHub and click on the green "Compare and pull request" button. 173 | 174 | Compare and pull request 175 | 176 | Describe your changes and submit your pull request 177 | 178 | Submit pull request 179 | 180 | ## What's next? 181 | 182 | 🎉 Congratulations 🎉 183 | 184 | You just took your first step as an open source contributor. Your pull request will be reviewed as soon as possible. 185 | Join us on [gitter](https://gitter.im/rebus-contributors/Lobby) if you have questions or need any help. If you feel like 186 | it, please give this repository a star ⭐. 187 | 188 | If you want something more to work on, look at [the open issues](https://github.com/ollelauribostrom/rebus/issues) for 189 | inspiration. Also, take a look the [Further Reading](https://github.com/ollelauribostrom/rebus#further-reading) section for more great learning resources. 190 | 191 | ## FAQ 192 | 193 | ### Resolve Merge Conflicts ### 194 | 195 | Once changes have been committed and staged it's time to manage conflicts by running: 196 | 197 | ```sh 198 | git pull upstream master 199 | ``` 200 | 201 | Head back to your favourite code editor and review any conflicts. Generally `current` changes will be highlighted in one color and `incoming` changes will be highlighted in a different color. `Accept` the `current` or `incoming` changes. 202 | 203 | Commit changes again as outlined in step #9 and #10 above or by running: 204 | 205 | ```sh 206 | git add -A 207 | ``` 208 | ```sh 209 | git commit -m "Your message" 210 | ``` 211 | ```sh 212 | git push origin 213 | ``` 214 | 215 | ### Linebreaks ### 216 | 217 | It is important to note that Windows and Linux operating systems deal with line endings differently. If you are getting an error where the program expects linebreaks to be "lf" but are finding "crlf" linebreak styles, 218 | 219 | Terminal linebreak error 220 | 221 | then you can run the following command in your terminal: 222 | 223 | ```sh 224 | npm run lint -- --fix 225 | ``` 226 | 227 | To avoid this problem in the future, you can change your editor to use an end of line sequence of 'lf'. To do this in Visual Studio Code, simply click on the lf/crlf button on the bottom right and then select 'lf' from the drop-down menu that appears. 228 | 229 | Visual Studio Code linebreaks 230 | 231 | ## Further Reading 232 | 233 | - [GitHub Open Source Guide](https://opensource.guide/) 234 | - [Resource To learn Git](https://try.github.io/) 235 | - [Git Tutorial Part 1: What is Version Control?](https://www.youtube.com/watch?v=9GKpbI1siow&feature=youtu.be) 236 | - [Git Tutorial Part 2: Vocab (Repo, Staging, Commit, Push, Pull)](https://www.youtube.com/watch?v=n-p1RUmdl9M) 237 | - [Git Tutorial Part 3: Installation, Command-line & Clone](https://www.youtube.com/watch?v=UFEby2zo-9E) 238 | - [Git Tutorial Part 4: GitHub (Pushing to a Server)](https://www.youtube.com/watch?v=ol_UCWox9kc) 239 | - [Git & GitHub Crash Course For Beginners](https://www.youtube.com/watch?v=SWYqp7iY_Tc) 240 | - [Git Magic](http://www-cs-students.stanford.edu/~blynn/gitmagic/index.html) 241 | - [Friendly Beginner Repos](https://github.com/MunGell/awesome-for-beginners) 242 | - [GitHub Endorsed Beginning Contributer Repos](https://github.com/showcases/great-for-new-contributors) 243 | - [Sourcetree - Git GUI for macOS and Windows](https://www.sourcetreeapp.com/) 244 | - [VS Code - extensible code editor](https://code.visualstudio.com/) 245 | - [GitHub Atom - Hackable Text Editor for the 21st Century](https://atom.io/) 246 | 247 | ## Support 248 | 249 | Please [open an issue](https://github.com/ollelauribostrom/rebus/issues/new) for support, or join us on [gitter](https://gitter.im/rebus-contributors/Lobby). 250 | 251 | ## Code of Conduct 252 | 253 | This project adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct). 254 | Please read the full text so that you can understand what actions will and will not be tolerated. 255 | 256 | ## License 257 | 258 | Licensed under the MIT License. 259 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = api => { 2 | api.cache(true); 3 | 4 | return { 5 | presets: [['@babel/preset-env', { targets: '> 1%, not dead' }]], 6 | plugins: ['@babel/plugin-transform-runtime'] 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollelauribostrom/rebus/37a09197fbe5ecfb497692ba159f076f55f1136e/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rebus", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "dependencies": { 6 | "@sentry/browser": "^4.2.4", 7 | "dom-confetti": "0.0.11" 8 | }, 9 | "devDependencies": { 10 | "@babel/core": "^7.1.2", 11 | "@babel/plugin-transform-runtime": "^7.1.0", 12 | "@babel/preset-env": "^7.0.0", 13 | "@babel/runtime": "^7.1.2", 14 | "babel-core": "^7.0.0-0", 15 | "babel-eslint": "^9.0.0", 16 | "babel-jest": "^23.6.0", 17 | "babel-loader": "^8.0.4", 18 | "coveralls": "^3.0.2", 19 | "css-loader": "^1.0.1", 20 | "eslint": "^5.8.0", 21 | "eslint-config-airbnb": "^17.1.0", 22 | "eslint-config-prettier": "^3.0.1", 23 | "eslint-plugin-import": "^2.14.0", 24 | "eslint-plugin-jsx-a11y": "^6.1.2", 25 | "eslint-plugin-prettier": "^2.7.0", 26 | "eslint-plugin-react": "^7.11.1", 27 | "html-loader": "^0.5.5", 28 | "html-webpack-plugin": "^3.2.0", 29 | "jest": "^23.6.0", 30 | "prettier": "^1.14.2", 31 | "prettier-eslint": "^8.8.2", 32 | "style-loader": "^0.23.1", 33 | "webpack": "^4.23.1", 34 | "webpack-cli": "^3.1.2", 35 | "webpack-dev-server": "^3.1.14" 36 | }, 37 | "scripts": { 38 | "start": "webpack-dev-server --mode development", 39 | "build": "webpack --mode production --env /rebus/", 40 | "test": "jest", 41 | "test:all": "npm run test && npm run lint", 42 | "coverage": "jest --coverage", 43 | "coveralls": "jest --coverage && cat ./coverage/lcov.info | coveralls", 44 | "lint": "eslint src tests" 45 | }, 46 | "jest": { 47 | "collectCoverageFrom": [ 48 | "/src/**/*.js" 49 | ], 50 | "coverageDirectory": "/coverage", 51 | "moduleNameMapper": { 52 | "\\.(css|sass)$": "/tests/__mocks__/styleMock.js" 53 | }, 54 | "globals": { 55 | "isTestRun": true 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/css/main.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Raleway'); 2 | 3 | html, 4 | body, 5 | .root, 6 | .app { 7 | width: 100%; 8 | height: 100%; 9 | padding: 0; 10 | margin: 0; 11 | overflow: hidden; 12 | } 13 | 14 | *:focus { 15 | outline: 0; 16 | } 17 | 18 | .root { 19 | background-color: #330000; 20 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 800 400'%3E%3Cdefs%3E%3CradialGradient id='a' cx='396' cy='281' r='514' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%23D18'/%3E%3Cstop offset='1' stop-color='%23330000'/%3E%3C/radialGradient%3E%3ClinearGradient id='b' gradientUnits='userSpaceOnUse' x1='400' y1='148' x2='400' y2='333'%3E%3Cstop offset='0' stop-color='%23FA3' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='%23FA3' stop-opacity='0.5'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect fill='url(%23a)' width='800' height='400'/%3E%3Cg fill-opacity='0.4'%3E%3Ccircle fill='url(%23b)' cx='267.5' cy='61' r='300'/%3E%3Ccircle fill='url(%23b)' cx='532.5' cy='61' r='300'/%3E%3Ccircle fill='url(%23b)' cx='400' cy='30' r='300'/%3E%3C/g%3E%3C/svg%3E"); 21 | background-attachment: fixed; 22 | background-size: cover; 23 | background-position: center; 24 | } 25 | 26 | .app { 27 | color: #fff; 28 | font-family: 'Raleway', sans-serif; 29 | display: grid; 30 | grid-template-rows: 1fr 3fr 1fr; 31 | grid-template-columns: 1fr 1fr 2fr 1fr 1fr; 32 | grid-column-gap: 30px; 33 | } 34 | 35 | .logo { 36 | position: absolute; 37 | top: 30px; 38 | left: 30px; 39 | } 40 | 41 | .rebus { 42 | background-color: #1542e642; 43 | grid-column-start: 3; 44 | align-self: center; 45 | grid-row-start: 2; 46 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); 47 | transition: 0.3s; 48 | display: grid; 49 | grid-template-rows: 40px auto; 50 | grid-row-gap: 30px; 51 | padding-bottom: 20px; 52 | } 53 | 54 | .rebus:hover { 55 | box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2); 56 | } 57 | 58 | .rebus--answered { 59 | background-color: #26bd88b0; 60 | } 61 | 62 | .rebus__header { 63 | background-color: #1d1d1d1a; 64 | font-size: 14px; 65 | color: #ffffff; 66 | display: flex; 67 | align-items: center; 68 | padding-left: 20px; 69 | } 70 | 71 | .rebus__symbols { 72 | align-self: center; 73 | justify-self: center; 74 | font-size: 3em; 75 | text-align: center; 76 | padding: 0 20px; 77 | } 78 | 79 | .rebus__words { 80 | padding: 0 20px; 81 | } 82 | 83 | .rebus__hint { 84 | text-align: center; 85 | grid-column-start: 2; 86 | grid-column-end: span 3; 87 | grid-row-start: 3; 88 | } 89 | 90 | .word { 91 | margin-bottom: 20px; 92 | display: flex; 93 | justify-content: center; 94 | align-items: center; 95 | } 96 | 97 | .word__char { 98 | height: 50px; 99 | width: 50px; 100 | border: 1px solid #fff; 101 | background-color: transparent; 102 | color: #fff; 103 | text-align: center; 104 | font-size: 24px; 105 | font-weight: 500; 106 | font-family: 'Raleway', sans-serif; 107 | text-transform: uppercase; 108 | } 109 | 110 | .word__char:not(:placeholder-shown) { 111 | background-color: #0000004d; 112 | } 113 | 114 | .word__char:active, 115 | .word__char:focus { 116 | background-color: #0000001f; 117 | } 118 | 119 | .change-button { 120 | background-color: transparent; 121 | border: 0; 122 | font-size: 20px; 123 | cursor: pointer; 124 | padding: 10px; 125 | align-self: center; 126 | justify-self: flex-start; 127 | } 128 | 129 | .change-button svg { 130 | height: 36px; 131 | } 132 | 133 | .change-button path { 134 | fill: #fff; 135 | } 136 | 137 | .change-button:hover path, 138 | .change-button:active path, 139 | .change-button:focus path { 140 | fill: rgba(239, 190, 7, 0.8); 141 | } 142 | 143 | .change-button--prev { 144 | grid-column-start: 2; 145 | grid-row-start: 2; 146 | justify-self: flex-end; 147 | } 148 | 149 | .change-button--next { 150 | transform: rotate(180deg); 151 | grid-column-start: 4; 152 | grid-row-start: 2; 153 | } 154 | 155 | .confetti-canon { 156 | position: absolute; 157 | left: 50%; 158 | top: 50%; 159 | transform: translate3d(-50%, -50%, 0); 160 | } 161 | 162 | .progress-bar { 163 | grid-row-start: 4; 164 | grid-column-start: 1; 165 | grid-column-end: 6; 166 | border: none; 167 | background: #9c0629; 168 | } 169 | 170 | progress[value] { 171 | width: auto; 172 | height: 20px; 173 | -webkit-appearance: none; 174 | } 175 | 176 | progress[value]::-webkit-progress-bar { 177 | background-color: #9c0629; 178 | } 179 | 180 | progress[value]::-moz-progress-bar { 181 | background-color: #438365; 182 | } 183 | 184 | progress[value]::-webkit-progress-value { 185 | background-color: #438365; 186 | } 187 | 188 | /* ---------------------------------------------- 189 | * GitHub Corners 190 | * w: https://github.com/tholman/github-corners 191 | * ---------------------------------------------- */ 192 | 193 | .github-corner { 194 | fill: #951e89; 195 | color: #fff; 196 | position: absolute; 197 | top: 0; 198 | border: 0; 199 | right: 0; 200 | } 201 | 202 | .github-corner:hover .octo-arm { 203 | animation: octocat-wave 560ms ease-in-out; 204 | } 205 | 206 | @keyframes octocat-wave { 207 | 0%, 208 | 100% { 209 | transform: rotate(0); 210 | } 211 | 20%, 212 | 60% { 213 | transform: rotate(-25deg); 214 | } 215 | 40%, 216 | 80% { 217 | transform: rotate(10deg); 218 | } 219 | } 220 | 221 | @media (min-width: 501px) and (max-width: 1024px) { 222 | .change-button--prev { 223 | grid-column-start: 1; 224 | } 225 | .change-button--next { 226 | grid-column-start: 5; 227 | } 228 | .rebus { 229 | grid-column-start: 2; 230 | grid-column-end: span 3; 231 | } 232 | .word { 233 | display: grid; 234 | grid-template-columns: repeat(auto-fit, 50px); 235 | } 236 | .word__char { 237 | width: auto; 238 | } 239 | } 240 | 241 | @media (max-width: 500px) { 242 | .logo { 243 | display: none; 244 | } 245 | .app { 246 | grid-template-rows: 1fr 8fr 2fr; 247 | } 248 | .github-corner { 249 | display: none; 250 | } 251 | .change-button { 252 | grid-row-start: 3; 253 | } 254 | .rebus { 255 | grid-row-start: 1; 256 | grid-row-end: span 2; 257 | grid-column-start: 1; 258 | grid-column-end: span 5; 259 | border-radius: 0; 260 | } 261 | .rebus__header { 262 | border-radius: 0; 263 | } 264 | .word { 265 | display: grid; 266 | grid-template-columns: repeat(auto-fit, 50px); 267 | } 268 | .word__char { 269 | width: auto; 270 | } 271 | } 272 | 273 | /* ---------------------------------------------- 274 | * Generated by Animista on 2018-10-2 9:2:14 275 | * w: http://animista.net, t: @cssanimista 276 | * ---------------------------------------------- */ 277 | 278 | .animation--flip-vertical-right { 279 | animation: flip-vertical-right 0.4s cubic-bezier(0.455, 0.03, 0.515, 0.955) both; 280 | } 281 | 282 | .animation--flip-vertical-left { 283 | animation: flip-vertical-left 0.4s cubic-bezier(0.455, 0.03, 0.515, 0.955) both; 284 | } 285 | 286 | .animation--shake { 287 | animation: shake 0.82s cubic-bezier(.36, .07, .19, .97) both; 288 | transform: translate3d(0, 0, 0); 289 | backface-visibility: hidden; 290 | perspective: 1000px; 291 | } 292 | 293 | /** 294 | * ---------------------------------------- 295 | * animation flip-vertical-right 296 | * ---------------------------------------- 297 | */ 298 | @keyframes flip-vertical-right { 299 | 0% { 300 | transform: rotateY(0); 301 | } 302 | 100% { 303 | transform: rotateY(360deg); 304 | } 305 | } 306 | 307 | /** 308 | * ---------------------------------------- 309 | * animation flip-vertical-left 310 | * ---------------------------------------- 311 | */ 312 | @keyframes flip-vertical-left { 313 | 0% { 314 | transform: rotateY(0); 315 | } 316 | 100% { 317 | transform: rotateY(-360deg); 318 | } 319 | } 320 | 321 | @keyframes shake { 322 | 10%, 90% { 323 | transform: translate3d(-1px, 0, 0); 324 | } 325 | 326 | 20%, 80% { 327 | transform: translate3d(2px, 0, 0); 328 | } 329 | 330 | 30%, 50%, 70% { 331 | transform: translate3d(-4px, 0, 0); 332 | } 333 | 334 | 40%, 60% { 335 | transform: translate3d(4px, 0, 0); 336 | } 337 | } -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollelauribostrom/rebus/37a09197fbe5ecfb497692ba159f076f55f1136e/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Rebus 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/js/app.js: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/browser'; 2 | import { render } from './mini'; 3 | import { App } from './components/App'; 4 | import { Logo } from './components/Logo'; 5 | import { GithubCorner } from './components/GithubCorner'; 6 | import { ChangeButton } from './components/ChangeButton'; 7 | import { Rebus } from './components/Rebus'; 8 | import { ProgressBar } from './components/ProgressBar'; 9 | import { Hint } from './components/Hint'; 10 | 11 | import { actions } from './store'; 12 | import '../css/main.css'; 13 | 14 | export function registerListeners() { 15 | document.addEventListener('keyup', event => { 16 | const key = event.key || event.keyCode; // For older browser support 17 | if (key === 'ArrowRight' || key === 39) { 18 | actions.next(); 19 | } 20 | if (key === 'ArrowLeft' || key === 37) { 21 | actions.prev(); 22 | } 23 | }); 24 | } 25 | 26 | export function setCurrentFromURL() { 27 | const params = new URLSearchParams(window.location.search); 28 | const id = Number(params.get('rebus')); 29 | actions.setCurrent(id); 30 | } 31 | 32 | export function init() { 33 | try { 34 | return render( 35 | App( 36 | Logo(), 37 | GithubCorner({ url: 'https://github.com/ollelauribostrom/rebus' }), 38 | ChangeButton({ 39 | className: 'change-button--prev', 40 | onClick: () => actions.prev() 41 | }), 42 | Rebus({ 43 | charInput: (input, wordIndex, charIndex) => { 44 | const confettiCanon = document.querySelector('.confetti-canon'); 45 | actions.setInput(input, wordIndex, charIndex); 46 | actions.check(confettiCanon); 47 | } 48 | }), 49 | ChangeButton({ 50 | className: 'change-button--next', 51 | onClick: () => actions.next() 52 | }), 53 | Hint(), 54 | ProgressBar() 55 | ), 56 | document.querySelector('.root') 57 | ); 58 | } catch (err) { 59 | return Sentry.captureException(err); 60 | } 61 | } 62 | 63 | if (!global || !global.isTestRun) { 64 | Sentry.init({ dsn: 'https://8f025bee12e84d9b8a16e9c3b9155ce8@sentry.io/1300214' }); 65 | init(); 66 | registerListeners(); 67 | setCurrentFromURL(); 68 | } 69 | -------------------------------------------------------------------------------- /src/js/components/App.js: -------------------------------------------------------------------------------- 1 | import { createComponent } from '../mini'; 2 | 3 | export function App(...children) { 4 | return createComponent({ 5 | children, 6 | render() { 7 | return ` 8 |
9 | 10 |
11 | `; 12 | } 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/js/components/ChangeButton.js: -------------------------------------------------------------------------------- 1 | import { createComponent } from '../mini'; 2 | 3 | export function ChangeButton(props) { 4 | return createComponent({ 5 | props, 6 | render({ className = '' }) { 7 | return ` 8 | 22 | `; 23 | } 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/js/components/Char.js: -------------------------------------------------------------------------------- 1 | import { createComponent } from '../mini'; 2 | 3 | export function Char(props) { 4 | return createComponent({ 5 | props, 6 | render({ current, rebuses, wordIndex, charIndex }) { 7 | const rebus = rebuses[current]; 8 | const previousWords = rebus.words.slice(0, wordIndex).join(''); 9 | const index = wordIndex > 0 ? previousWords.length + charIndex : charIndex; 10 | const value = rebus.input[index] || ''; 11 | return ` 12 | `; 20 | } 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/js/components/GithubCorner.js: -------------------------------------------------------------------------------- 1 | import { createComponent } from '../mini'; 2 | 3 | export function GithubCorner(props) { 4 | return createComponent({ 5 | props, 6 | render({ url }) { 7 | return ` 8 | 9 | 16 | 17 | `; 18 | } 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/js/components/Hint.js: -------------------------------------------------------------------------------- 1 | import { createComponent } from '../mini'; 2 | import { connect } from '../store'; 3 | 4 | export function Hint(props) { 5 | return connect( 6 | createComponent({ 7 | props, 8 | render({ current, rebuses, incorrectAnswerCount }) { 9 | const INCORRECT_ANSWER_MAX_COUNT = 3; 10 | const HINT_SYMBOL = '💡'; 11 | const rebus = rebuses[current]; 12 | const showHint = 13 | incorrectAnswerCount >= INCORRECT_ANSWER_MAX_COUNT && !rebus.isAnswered && rebus.hint; 14 | return ` 15 | ${ 16 | showHint 17 | ? ` 18 | ${HINT_SYMBOL} ${rebus.hint} 19 | ` 20 | : '' 21 | } 22 | `; 23 | } 24 | }) 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/js/components/Logo.js: -------------------------------------------------------------------------------- 1 | import { createComponent } from '../mini'; 2 | 3 | export function Logo() { 4 | return createComponent({ 5 | render() { 6 | return ` 7 | 18 | `; 19 | } 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/js/components/ProgressBar.js: -------------------------------------------------------------------------------- 1 | import { createComponent } from '../mini'; 2 | import { connect } from '../store'; 3 | 4 | export function ProgressBar(props) { 5 | return connect( 6 | createComponent({ 7 | props, 8 | render({ rebuses }) { 9 | const answeredRebusCount = rebuses.filter(rebus => rebus.isAnswered).length; 10 | return ` 11 | 12 | 13 | `; 14 | } 15 | }) 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/js/components/Rebus.js: -------------------------------------------------------------------------------- 1 | import { createComponent } from '../mini'; 2 | import { connect } from '../store'; 3 | import { Word } from './Word'; 4 | 5 | export function Rebus(props, ...children) { 6 | return connect( 7 | createComponent({ 8 | props, 9 | children, 10 | componentDidMount() { 11 | const rebus = this.props.rebuses[this.props.current]; 12 | if (rebus.isAnswered) { 13 | this.$parent.querySelector('.change-button--next').focus(); 14 | } else { 15 | this.$element.querySelector('input').focus(); 16 | } 17 | }, 18 | componentDidUpdate() { 19 | const rebus = this.props.rebuses[this.props.current]; 20 | /* If history API isn't available, we shouldn't revert to the more widely available `window.location.href`, 21 | as it incurs a new HTTP request and thus results in an infinite loop (and breaks SPAs). */ 22 | if (window.history) { 23 | // Adds 'rebus' query parameter to end of URL. Should be endpoint-agnostic. 24 | window.history.pushState('', '', `?rebus=${rebus.id}`); 25 | } 26 | if (rebus.isAnswered) { 27 | this.$parent.querySelector('.change-button--next').focus(); 28 | } else { 29 | this.$element.querySelector('input').focus(); 30 | } 31 | }, 32 | render({ current, rebuses, animation }) { 33 | const rebus = rebuses[current]; 34 | this.children = rebus.words.map((word, wordIndex) => 35 | Word({ word, wordIndex, current, rebuses, charInput: props.charInput }) 36 | ); 37 | return ` 38 |
39 |
40 | ${current + 1}/${rebuses.length} 41 |
42 | ${rebus.symbols.join(' ')} 43 |
44 | 45 |
46 |
47 | `; 48 | } 49 | }) 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/js/components/Word.js: -------------------------------------------------------------------------------- 1 | import { createComponent } from '../mini'; 2 | import { Char } from './Char'; 3 | import { actions } from '../store'; 4 | 5 | export function Word(props, ...children) { 6 | return createComponent({ 7 | props, 8 | children, 9 | render({ word, charInput }) { 10 | this.children = word.split('').map((_, charIndex) => 11 | Char({ 12 | charIndex, 13 | ...props, 14 | onInput: e => { 15 | const input = e.target.value; 16 | charInput(input, props.wordIndex, charIndex); 17 | 18 | if (/[a-zA-Z]/.test(input)) { 19 | const nextChild = e.target.nextElementSibling; 20 | const hasNextWord = !!this.$element.nextSibling; 21 | 22 | if (nextChild !== null) { 23 | nextChild.focus(); 24 | } 25 | if (nextChild === null && hasNextWord) { 26 | this.$element.nextSibling.firstElementChild.focus(); 27 | } 28 | } 29 | }, 30 | onKeydown: e => { 31 | const { key, keyCode, target } = e; 32 | if (keyCode >= 65 && keyCode <= 90) { 33 | target.value = ''; 34 | } 35 | if (key === 'Enter' || keyCode === 13) { 36 | actions.shake(); 37 | } 38 | if (key === 'Backspace' || keyCode === 8) { 39 | const input = target.value; 40 | const prevChild = target.previousElementSibling; 41 | if (prevChild !== null && input === '') { 42 | prevChild.focus(); 43 | e.preventDefault(); 44 | } 45 | } 46 | } 47 | }) 48 | ); 49 | return ` 50 |
51 | 52 |
53 | `; 54 | } 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/js/mini/component.js: -------------------------------------------------------------------------------- 1 | import { renderComponent } from './render'; 2 | import { isFunction } from './utils'; 3 | 4 | export function createComponent({ 5 | props = {}, 6 | children = [], 7 | render, 8 | componentDidMount, 9 | componentDidUpdate 10 | }) { 11 | return { 12 | props, 13 | children, 14 | render, 15 | componentDidMount, 16 | componentDidUpdate, 17 | update() { 18 | if (this.rendered !== this.render(this.props)) { 19 | renderComponent(this, this.$parent); 20 | if (isFunction(this.componentDidUpdate)) { 21 | this.componentDidUpdate(); 22 | } 23 | } 24 | } 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/js/mini/index.js: -------------------------------------------------------------------------------- 1 | export { createComponent } from './component'; 2 | export { render } from './render'; 3 | export { createStore } from './store'; 4 | -------------------------------------------------------------------------------- /src/js/mini/render.js: -------------------------------------------------------------------------------- 1 | import { isFunction, isEventCallback, getEventName, createHTMLElement } from './utils'; 2 | 3 | export function addListeners(component, $element) { 4 | Object.entries(component.props).forEach(([key, value]) => { 5 | if (isEventCallback(key, value)) { 6 | const event = getEventName(key); 7 | $element.addEventListener(event, value); 8 | } 9 | }); 10 | } 11 | 12 | export function callComponentDidMount(component) { 13 | if (isFunction(component.componentDidMount)) { 14 | component.componentDidMount(); 15 | } 16 | component.children.forEach(child => callComponentDidMount(child)); 17 | } 18 | 19 | export function renderComponent(component, $parent) { 20 | const rendered = component.render(component.props); 21 | const $element = createHTMLElement(rendered); 22 | const placeholder = $element.querySelector('children'); 23 | if (component.children && placeholder) { 24 | component.children.forEach(child => { 25 | const childComponent = renderComponent(child, $element); 26 | placeholder.parentNode.insertBefore(childComponent, placeholder); 27 | }); 28 | placeholder.parentNode.removeChild(placeholder); 29 | } 30 | if (component.$element) { 31 | component.$parent.replaceChild($element, component.$element); 32 | } 33 | addListeners(component, $element); 34 | component.$parent = $parent; // eslint-disable-line 35 | component.$element = $element; // eslint-disable-line 36 | component.rendered = rendered; // eslint-disable-line 37 | return $element; 38 | } 39 | 40 | export function render(component, root) { 41 | const element = renderComponent(component, root); 42 | root.appendChild(element); 43 | callComponentDidMount(component); 44 | } 45 | -------------------------------------------------------------------------------- /src/js/mini/store.js: -------------------------------------------------------------------------------- 1 | export function createStore(initialState, actions) { 2 | const store = { 3 | connectedComponents: [], 4 | state: initialState 5 | }; 6 | store.actions = new Proxy(actions, { 7 | get(target, key) { 8 | return async (...args) => { 9 | const newState = await actions[key](store.state, ...args); 10 | store.state = Object.assign(store.state, newState); 11 | store.connectedComponents.forEach(c => c.update()); 12 | }; 13 | } 14 | }); 15 | store.connect = component => { 16 | const connected = new Proxy(component, { 17 | get(target, key) { 18 | if (key === 'render') { 19 | return (...args) => target.render(store.state, ...args); 20 | } 21 | if (key === 'props') { 22 | return Object.assign({}, target.props, store.state); 23 | } 24 | return Reflect.get(target, key); 25 | } 26 | }); 27 | store.connectedComponents.push(connected); 28 | return connected; 29 | }; 30 | return store; 31 | } 32 | -------------------------------------------------------------------------------- /src/js/mini/utils.js: -------------------------------------------------------------------------------- 1 | export function isFunction(value) { 2 | return typeof value === 'function'; 3 | } 4 | 5 | export function getEventName(key) { 6 | return key.substr(2).toLowerCase(); 7 | } 8 | 9 | export function isEventCallback(key, value) { 10 | return isFunction(value) && key.substr(0, 2) === 'on'; 11 | } 12 | 13 | export function createHTMLElement(HTMLString) { 14 | const template = document.createElement('template'); 15 | template.innerHTML = HTMLString; 16 | return template.content.firstElementChild; 17 | } 18 | -------------------------------------------------------------------------------- /src/js/rebuses.js: -------------------------------------------------------------------------------- 1 | const rebuses = [ 2 | { 3 | symbols: ['Re', '+', '🚌'], 4 | words: ['Rebus'], 5 | hint: 'You´re solving one right now' 6 | }, 7 | { 8 | symbols: ['🏠', '+', 'pl', '+', '🐜', '+', 's'], 9 | words: ['Houseplants'], 10 | hint: `The second emoji is 'ant' not 'bug'` 11 | }, 12 | { 13 | symbols: ['📖', '+', '🙋', '+', '📝'], 14 | words: ['Readme', 'file'], 15 | hint: 'The default markdown file of every GitHub repo' 16 | }, 17 | { 18 | symbols: ['🚗', '+', 'a', '+', '🚐'], 19 | words: ['Caravan'], 20 | hint: 'The trailer you take when you go camping' 21 | }, 22 | { 23 | symbols: ['⭐', '+', '🐠'], 24 | words: ['Starfish'], 25 | hint: 'Say the two emojis out loud' 26 | }, 27 | { 28 | symbols: ['💡', '+', '🏠'], 29 | words: ['Lighthouse'], 30 | hint: 'A tower with a light to guide ships at sea' 31 | }, 32 | { 33 | symbols: ['🌲', '+', '🍎'], 34 | words: ['Pineapple'], 35 | hint: 'Who lives in a ____ under the sea? SPONGEBOB SQUAREPANTS!' 36 | }, 37 | { 38 | symbols: ['🥚', '+', '🌱'], 39 | words: ['Eggplant'], 40 | hint: '🍆' 41 | }, 42 | { 43 | symbols: ['🔥', '+', '🦊'], 44 | words: ['Firefox'], 45 | hint: 'One of the most popular web browsers' 46 | }, 47 | { 48 | symbols: ['💊', '+', 'ow'], 49 | words: ['Pillow'], 50 | hint: 'Soft item to rest your head when you sleep' 51 | }, 52 | { 53 | symbols: ['🖊️', '+', 'd', '+', '🐜'], 54 | words: ['Pendant'], 55 | hint: `The second emoji is 'ant' not 'bug'` 56 | }, 57 | { 58 | symbols: ['🌡️', '+', '🔑'], 59 | words: ['Hotkey'] 60 | }, 61 | { 62 | symbols: ['🌞', '+', '🛀'], 63 | words: ['Sunbath'], 64 | hint: 'A way for cold-blooded animals to keep warm' 65 | }, 66 | { 67 | symbols: ['🚗', '🚙', '🚗'], 68 | words: ['Cars'], 69 | hint: `They're always on the road` 70 | }, 71 | { 72 | symbols: ['👦🏻', '+', '⚡', '+', '👓', '=', '✨'], 73 | words: ['Harry', 'Potter'], 74 | hint: 'Hogwarts' 75 | }, 76 | { 77 | symbols: ['H', '=', 'C', '👒'], 78 | words: ['Cat'], 79 | hint: `It's raining ____ and dogs` 80 | }, 81 | { 82 | symbols: ['FR', '=', 'D', '🐸'], 83 | words: ['Dog'], 84 | hint: `Man's best friend` 85 | }, 86 | { 87 | symbols: ['📦', 'B', '=', 'F'], 88 | words: ['Fox'], 89 | hint: `Say the emoji out loud and replace the 'B' with 'F'` 90 | }, 91 | { 92 | symbols: ['G', '+', '❤️'], 93 | words: ['Glove'], 94 | hint: `It covers your hand when it's cold outside` 95 | }, 96 | { 97 | symbols: ['🍏', '-', '🐒', '+', '🐜'], 98 | words: ['Plant'], 99 | hint: `It grows in your garden` 100 | }, 101 | { 102 | symbols: ['🐝', '🍁'], 103 | words: ['Belief'], 104 | hint: `Hmm, there's a bee and a leaf. Say both out loud` 105 | }, 106 | { 107 | symbols: ['🐱', '+', 'as', '+', '🏆', 'y̶', '+', 'e'], 108 | words: ['Catastrophe'], 109 | hint: 'A really really bad occurence' 110 | }, 111 | { 112 | symbols: ['🍑', '🍑', '+', 'i', 'n', 'a', 't', 'e'], 113 | words: ['Assassinate'], 114 | hint: `What's another word for your butt? That's what the emojis mean` 115 | }, 116 | { 117 | symbols: ['🍌', '+', '🍞'], 118 | words: ['Bananabread'], 119 | hint: `A monkey's favourite bread` 120 | }, 121 | { 122 | symbols: ['🔑', '🐗', '+', 'd'], 123 | words: ['Keyboard'], 124 | hint: `You're typing with one` 125 | }, 126 | { 127 | symbols: ['💵', '+', 'ew'], 128 | words: ['cashew'], 129 | hint: 'A type of nut' 130 | }, 131 | { 132 | symbols: ['🤘🏻', '+', '⭐️'], 133 | words: ['rockstar'], 134 | hint: 'Mick Jagger is a what?' 135 | }, 136 | { 137 | symbols: ['Tu', '+', '👄'], 138 | words: ['Tulip'], 139 | hint: 'This popular flower blooms in Spring' 140 | }, 141 | { 142 | symbols: ['🔥', '+', '🐶'], 143 | words: ['Hotdog'], 144 | hint: 'A close relative of burgers' 145 | }, 146 | { 147 | symbols: ['🌙', '+', '☀'], 148 | words: ['Moonshine'], 149 | hint: `Illegal alcohol` 150 | }, 151 | { 152 | symbols: ['💻', '+', '🌊'], 153 | words: ['Digital', 'Ocean'], 154 | hint: 'Similar to AWS and Microsoft Azure' 155 | }, 156 | { 157 | symbols: ['🔥', '+', '🌬️', '+', '🎈'], 158 | words: ['Hot', 'Air', 'Balloon'], 159 | hint: `Explore the skys with this` 160 | }, 161 | { 162 | symbols: ['💵', '+', '⚽'], 163 | words: ['Moneyball'], 164 | hint: `A great baseball movie` 165 | }, 166 | { 167 | symbols: ['🐂', '+', '👀'], 168 | words: ['Bullseye'], 169 | hint: 'The center/smallest part of a target' 170 | }, 171 | { 172 | symbols: ['🦇', '+', 'tery'], 173 | words: ['Battery'], 174 | hint: 'All your devices contain one' 175 | }, 176 | { 177 | symbols: ['🔒', '+', 'smith'], 178 | words: ['Locksmith'], 179 | hint: 'This person lets you into your home when you lose your keys' 180 | }, 181 | { 182 | symbols: ['👞', '+', 'horn'], 183 | words: ['Shoehorn'], 184 | hint: `Helps you put your shoe on` 185 | }, 186 | { 187 | symbols: ['🖊', '+', 't', '+', '🏠'], 188 | words: ['Penthouse'], 189 | hint: `The highest room in a hotel` 190 | }, 191 | { 192 | symbols: ['🐱', '+', 'e', '+', '💍'], 193 | words: ['Catering'], 194 | hint: 'Spell out the emojis then pronounce the word' 195 | }, 196 | { 197 | symbols: ['2', '+', 'Na️', '+', '🐟'], 198 | words: ['tunafish'], 199 | hint: 'A key ingredient in tuna salad' 200 | }, 201 | { 202 | symbols: ['🔥', '+', 'man'], 203 | words: ['Fireman'], 204 | hint: 'Helps put out large fires' 205 | }, 206 | { 207 | symbols: ['💡', '+', 'er'], 208 | words: ['Lighter'], 209 | hint: 'Used to ignite cigarettes' 210 | }, 211 | { 212 | symbols: ['🌙', '+', '💡'], 213 | words: ['Moonlight'], 214 | hint: `Lights up the night` 215 | }, 216 | { 217 | symbols: ['f', '+', '🌬️', '+', 'y'], 218 | words: ['Fairy'], 219 | hint: 'The tooth ____ changes lost teeth to money' 220 | }, 221 | { 222 | symbols: ['🔨', '+', '⏰'], 223 | words: ['Hammer', 'time'], 224 | hint: `MC Hammer's popular one-liner` 225 | }, 226 | { 227 | symbols: ['👻', '+', '🚂'], 228 | words: ['Ghost', 'Train'], 229 | hint: `A haunted train` 230 | }, 231 | { 232 | symbols: ['🍦', '+', '🍦', '+', '👶'], 233 | words: ['Ice', 'Ice', 'Baby'], 234 | hint: `A great hit by Vanilla Ice` 235 | }, 236 | { 237 | symbols: ['🚗', '+', 'D', '+', '👹', '+', '🐏'], 238 | words: ['cardiogram'], 239 | hint: `Graph showing your heart's activity over a period of time` 240 | }, 241 | { 242 | symbols: ['🌎', '+', 'wide', '+', '🕸️'], 243 | words: ['world', 'wide', 'web'], 244 | hint: `What does the 'www' in websites stand for?` 245 | }, 246 | { 247 | symbols: ['🐀', '+', 'AT', '+', '2️', '+', 'E'], 248 | words: ['ratatouille'], 249 | hint: 'A rat chef' 250 | }, 251 | { 252 | symbols: ['📖', '+', '🔑', '+', 'ping'], 253 | words: ['bookkeeping'], 254 | hint: 'Maintaining financial records of a business' 255 | }, 256 | { 257 | symbols: ['sc', '+', '🦍', '+', '🐐'], 258 | words: ['scapegoat'], 259 | hint: 'Just say it out loud' 260 | }, 261 | { 262 | symbols: ['🐜', '+', '⛵️', '+', 'tica'], 263 | words: ['antarctica'], 264 | hint: 'The southernmost continent on the planet' 265 | }, 266 | { 267 | symbols: ['🌧', '+', '🏹'], 268 | words: ['rainbow'], 269 | hint: 'You see this after it rains' 270 | }, 271 | { 272 | symbols: ['❄️', '+', '⚾️'], 273 | words: ['snowball'], 274 | hint: `Throw them at your friends in winter` 275 | }, 276 | { 277 | symbols: ['❄️', '+', 'man'], 278 | words: ['snowman'], 279 | hint: 'You make this when its snowing' 280 | }, 281 | { 282 | symbols: ['🐏', '📃'], 283 | words: ['rampage'], 284 | hint: `The Hulk likes to go on a ____` 285 | }, 286 | { 287 | symbols: ['🐈', '+', '🥊'], 288 | words: ['cat', 'fight'], 289 | hint: 'AKA girl fight' 290 | }, 291 | { 292 | symbols: ['👣', '+', '📝'], 293 | words: ['footnote'], 294 | hint: 'Placed at the bottom of a page to describe something referenced in the page' 295 | }, 296 | { 297 | symbols: ['🐝', '+', 'r'], 298 | words: ['Beer'], 299 | hint: '🍺' 300 | }, 301 | { 302 | symbols: ['🎉', '-', 'Y', '+', '⏰', '+', 'Job'], 303 | words: ['part', 'time', 'job'], 304 | hint: `If 🎉 means 'party', remove the 'y'` 305 | }, 306 | { 307 | symbols: ['🌲', '+', '🏠'], 308 | words: ['treehouse'], 309 | hint: 'A house built in a tree' 310 | }, 311 | { 312 | symbols: ['✔', '+', 'list'], 313 | words: ['checklist'], 314 | hint: 'What you might use to keep track of your tasks for the day' 315 | }, 316 | { 317 | symbols: ['💧', '+', '📦'], 318 | words: ['Dropbox'], 319 | hint: 'A popular cloud storage company; say the emojis out loud' 320 | }, 321 | { 322 | symbols: ['💡', '+', '⚔️'], 323 | words: ['Lightsaber'], 324 | hint: 'The Star Wars weapon for hand-to-hand combat' 325 | }, 326 | { 327 | symbols: ['☕️'], 328 | words: ['Java'], 329 | hint: 'A highly popular programming language owned by Oracle' 330 | }, 331 | { 332 | symbols: ['⎈'], 333 | words: ['Helm'], 334 | hint: 'Where you steer the boat' 335 | }, 336 | { 337 | symbols: ['🌍', '+', 'form'], 338 | words: ['Terraform'], 339 | hint: `A code software HashiCorp` 340 | }, 341 | { 342 | symbols: ['Uni', '+', '🌽'], 343 | words: ['unicorn'], 344 | hint: 'A billion-dollar startup company' 345 | }, 346 | { 347 | symbols: ['🆘', '+', 'desk'], 348 | words: ['helpdesk'], 349 | hint: `Where you go to find help` 350 | }, 351 | { 352 | symbols: ['🐱', '+', 'er', '+', '🗼'], 353 | words: ['caterpillar'], 354 | hint: 'It chews on leaves and then grows into something pretty.' 355 | }, 356 | { 357 | symbols: ['🧢', '+', 'tain'], 358 | words: ['captain'], 359 | hint: '_____ America 💪' 360 | }, 361 | { 362 | symbols: ['🦊', '+', 'hole'], 363 | words: ['foxhole'], 364 | hint: `A multiplayer strategy action game` 365 | }, 366 | { 367 | symbols: ['🐦', '+', 's-', '+', '👁️'], 368 | words: ["bird's-eye"], 369 | hint: `When you observe from above` 370 | }, 371 | { 372 | symbols: ['🐴', '+', '🔙'], 373 | words: ['horseback'], 374 | hint: `A cowboy's primary form of travel` 375 | }, 376 | { 377 | symbols: ['🎼', '+', '🍴'], 378 | words: ['pitchfork'], 379 | hint: `Angry mobs are frequently seen with this` 380 | }, 381 | { 382 | symbols: ['🌊', '+', '🐴'], 383 | words: ['seahorse'], 384 | hint: `One of Nemo's friends was a _______` 385 | }, 386 | { 387 | symbols: ['🔙', '+', '💀'], 388 | words: ['backbone'], 389 | hint: 'Another name for your spine' 390 | }, 391 | { 392 | symbols: ['💥', '+', '🌽'], 393 | words: ['popcorn'], 394 | hint: 'You watch movies with ___ and a drink' 395 | }, 396 | { 397 | symbols: ['🔥', '+', '🏠'], 398 | words: ['firehouse'], 399 | hint: `A very hot house` 400 | }, 401 | { 402 | symbols: ['🚗', '+', 'go'], 403 | words: ['cargo'], 404 | hint: 'Items carried by ships and planes' 405 | }, 406 | { 407 | symbols: ['sm', '+', '🎨'], 408 | words: ['smart'], 409 | hint: `A synonym for 'intelligent'` 410 | }, 411 | { 412 | symbols: ['😢', '+', '👶'], 413 | words: ['crybaby'], 414 | hint: 'Someone who complains a lot' 415 | }, 416 | { 417 | symbols: ['📻', '+', '🙂'], 418 | words: ['radiohead'], 419 | hint: `An old English rock band` 420 | }, 421 | { 422 | symbols: ['🗃', '+', 'elder', '+', '🐜'], 423 | words: ['Box', 'Elder', 'Bug'], 424 | hint: `An annoying species of true bug` 425 | }, 426 | { 427 | symbols: ['L', '+', '🏹'], 428 | words: ['Elbow'], 429 | hint: 'The joint in your arm below your shoulder' 430 | }, 431 | { 432 | symbols: ['📅', '+', 'et', '+', '🌍'], 433 | words: ['Planet', 'Earth'], 434 | hint: `Where we all live` 435 | }, 436 | { 437 | symbols: ['🔨', '+', 'head', '+', '🦈'], 438 | words: ['hammerhead', 'shark'], 439 | hint: 'A type of shark' 440 | }, 441 | { 442 | symbols: ['👩‍', '+', '💍'], 443 | words: ['red', 'herring'], 444 | hint: `A tasty seafood meal` 445 | }, 446 | { 447 | symbols: ['Black', '+', '🎩'], 448 | words: ['Blackhat'], 449 | hint: 'A type of hacker' 450 | }, 451 | { 452 | symbols: ['👁', '+', '💧'], 453 | words: ['Eyedrop'], 454 | hint: 'Used to treat irritated/inflammed eyes' 455 | }, 456 | { 457 | symbols: ['🧠', '+', '🌪', '+', 'ing'], 458 | words: ['Brainstorming'], 459 | hint: 'Thinking about ideas' 460 | }, 461 | { 462 | symbols: ['👞', '+', '📦'], 463 | words: ['Shoebox'], 464 | hint: 'A box you get shoes in' 465 | }, 466 | { 467 | symbols: ['💵', '+', '🛍️'], 468 | words: ['Money', 'Bag'], 469 | hint: `Every robbers desire` 470 | }, 471 | { 472 | symbols: ['💥', '+', 'py'], 473 | words: ['Poppy'], 474 | hint: `They make opium from this` 475 | }, 476 | { 477 | symbols: ['🍏', '+', '🥧'], 478 | words: ['Applepie'], 479 | hint: `It's as American as ______` 480 | }, 481 | { 482 | symbols: ['🍯', '+', '🌗'], 483 | words: ['Honeymoon'], 484 | hint: 'Comes right after a wedding' 485 | }, 486 | { 487 | symbols: ['🕷', '+', '🧔🏻'], 488 | words: ['Spiderman'], 489 | hint: `Uncle Ben's superhero nephew` 490 | }, 491 | { 492 | symbols: ['🐲', '+', '🍑'], 493 | words: ['Dragonfruit'], 494 | hint: `A mythical fruit made reality` 495 | }, 496 | { 497 | symbols: ['🔫-s', '+', 'and', '+', '🥀-s'], 498 | words: ['Gunsandroses'], 499 | hint: 'A popular rock band' 500 | }, 501 | { 502 | symbols: ['👦🏼', '+', '📔'], 503 | words: ['Facebook'], 504 | hint: `Mark Zuckerberg's company` 505 | }, 506 | { 507 | symbols: ['T', '+', '🏃', '+', '🐱', '+', 'e'], 508 | words: ['Truncate'], 509 | hint: 'To make something shorter by cutting off the end' 510 | }, 511 | { 512 | symbols: ['🐶', '+', '🐱', '+', 'cher'], 513 | words: ['Dogcatcher'], 514 | hint: `In case your dog runs away` 515 | }, 516 | { 517 | symbols: ['🌍', '+', '⭐️'], 518 | words: ['Earthstar'], 519 | hint: `A star shaped fungi` 520 | }, 521 | { 522 | symbols: ['👱', '+', '👨‍🎓', '+', '⛵️-s'], 523 | words: ['Headmasterships'], 524 | hint: `The role of the headmaster` 525 | }, 526 | { 527 | symbols: ['🅰️', '+', '💣', '+', 'in', '+', '🅰️', '+', '🐂'], 528 | words: ['Abominable'], 529 | hint: 'The ___ Snowman' 530 | }, 531 | { 532 | symbols: ['👁', '+', '💖', '+', 'YOU'], 533 | words: ['I', 'Love', 'You'], 534 | hint: '3 words, 8 letters' 535 | }, 536 | { 537 | symbols: ['🐴', '+', '👞'], 538 | words: ['Horse', 'Shoe'], 539 | hint: 'Read the emojis out loud' 540 | }, 541 | { 542 | symbols: ['🌍', '+', '☕'], 543 | words: ['World', 'Cup'], 544 | hint: 'A football/soccer tournament played every 4 years and organized by FIFA' 545 | }, 546 | { 547 | symbols: ['🔁', '+', '🔙'], 548 | words: ['Loopback'], 549 | hint: `Another word for rewind` 550 | }, 551 | { 552 | symbols: ['🔙', '+', '🏁'], 553 | words: ['Backend'], 554 | hint: `The opposite of frontend` 555 | }, 556 | { 557 | symbols: ['🌃', '+', '🐎'], 558 | words: ['Nightmare'], 559 | hint: 'A bad dream' 560 | }, 561 | { 562 | symbols: ['🔙', '+', '🚪'], 563 | words: ['Backdoor'], 564 | hint: `When the front door doesn't work` 565 | }, 566 | { 567 | symbols: ['🍀', '+', '🌌‍', '+', '🚶'], 568 | words: ['Luke Skywalker'], 569 | hint: 'Star Wars: the other Skywalker (not Anakin)' 570 | }, 571 | { 572 | symbols: ['👋', '+', '🌍'], 573 | words: ['Hello', 'World'], 574 | hint: 'The popular output of many introductory computer programming tutorials' 575 | }, 576 | { 577 | symbols: ['🌽', '+', 'u', '+', '👮', '+', 'ia'], 578 | words: ['Cornucopia'], 579 | hint: 'The horn of plenty' 580 | }, 581 | { 582 | symbols: ['💨', '+', '🦉', '-', 'L'], 583 | words: ['window'], 584 | hint: `The competitor of Apple, without the 's' at the end` 585 | }, 586 | { 587 | symbols: ['❌', '+', '🔤'], 588 | words: ['crossword'], 589 | hint: 'A type of puzzle' 590 | }, 591 | { 592 | symbols: ['S', '+', '🦀', '+', 'BLE'], 593 | words: ['scrabble'], 594 | hint: 'Words With Friends is based on this game' 595 | }, 596 | { 597 | symbols: ['🦇', '+', '👨'], 598 | words: ['Batman'], 599 | hint: 'Bruce Wayne' 600 | }, 601 | { 602 | symbols: ['☀️', '+', '👓'], 603 | words: ['Sunglasses'], 604 | hint: 'Prevents you from squinting all day at the beach' 605 | }, 606 | { 607 | symbols: ['🐄', '+', '👦'], 608 | words: ['Cowboy'], 609 | hint: 'Heroes of the wild west' 610 | }, 611 | { 612 | symbols: ['🖊️', '+', '👬'], 613 | words: ['Penpals'], 614 | hint: 'Friends who write letters to themselves' 615 | }, 616 | { 617 | symbols: ['⭐', '+', '💰'], 618 | words: ['Starbucks'], 619 | hint: 'A popular American coffee company based in Seattle, Washington' 620 | }, 621 | { 622 | symbols: ['📲', '+', '🅰️', '+', '🐀'], 623 | words: ['Apparat'], 624 | hint: `A german electronic musician` 625 | }, 626 | { 627 | symbols: ['☀️', '+', '🌼'], 628 | words: ['Sunflower'], 629 | hint: `A plant that always faces the sun` 630 | }, 631 | { 632 | symbols: ['⛓️', '+', '🔁', '+', '🎬'], 633 | words: ['Chainreaction'], 634 | hint: 'A series of events triggering each other' 635 | }, 636 | { 637 | symbols: ['🗑️', '+', '🏀'], 638 | words: ['Basketball'], 639 | hint: `Michael Jordan's primary sport` 640 | }, 641 | { 642 | symbols: ['🌎', '+', '🐛'], 643 | words: ['Earthworm'], 644 | hint: `It's not a moonworm` 645 | }, 646 | { 647 | symbols: ['🌬', '+', '🐟'], 648 | words: ['Blowfish'], 649 | hint: `A fish that swells up` 650 | }, 651 | { 652 | symbols: ['🔑', '+', '🕳'], 653 | words: ['Keyhole'], 654 | hint: 'Your house key goes into the ______' 655 | }, 656 | { 657 | symbols: ['💔', '+', '🕐'], 658 | words: ['Breaktime'], 659 | hint: `Everyone's favourite part of the day` 660 | }, 661 | { 662 | symbols: ['🐊', '+', '😭'], 663 | words: ['Crocodile', 'tears'], 664 | hint: `Fake tears` 665 | }, 666 | { 667 | symbols: ['💋', '☠'], 668 | words: ['Kiss', 'of', 'death'], 669 | hint: `One kiss you don't want to receive` 670 | }, 671 | { 672 | symbols: ['👨', '💀', '⛵'], 673 | words: ['Pirate'], 674 | hint: 'Criminals who ransacked ships in the 1700s-1900s' 675 | }, 676 | { 677 | symbols: ['🚪', '+', '🔔'], 678 | words: ['Doorbell'], 679 | hint: `Used to alert the occupant's of a house that somebody is at their door` 680 | }, 681 | { 682 | symbols: ['🌙', '+', '🎂'], 683 | words: ['Mooncake'], 684 | hint: `A lunar Chinese snack` 685 | }, 686 | { 687 | symbols: ['🐎', '+', '👟'], 688 | words: ['Horseshoe'], 689 | hint: `Protect's a horse's hooves` 690 | }, 691 | { 692 | symbols: ['⌚', '+', '👨'], 693 | words: ['Watchman'], 694 | hint: 'Who watches the ______?' 695 | }, 696 | { 697 | symbols: ['✋', '+', '📗'], 698 | words: ['Handbook'], 699 | hint: 'An instructional manual' 700 | }, 701 | { 702 | symbols: ['🛤️', '+', 'men'], 703 | words: ['Railwayman'], 704 | hint: `Works in a railway yard` 705 | }, 706 | { 707 | symbols: ['🐱', '+', '🐠'], 708 | words: ['Catfish'], 709 | hint: 'A person who fakes his/her identity online' 710 | }, 711 | { 712 | symbols: ['👂', '+', '💍'], 713 | words: ['Earring'], 714 | hint: 'Say the emojis out loud' 715 | }, 716 | { 717 | symbols: ['10', '+', '🐜'], 718 | words: ['Tenant'], 719 | hint: 'A person renting out an apartment' 720 | }, 721 | { 722 | symbols: ['⛵️', '+', 'ment'], 723 | words: ['Shipment'], 724 | hint: `A package being delivered` 725 | }, 726 | { 727 | symbols: ['2', '+', '🐝', '+', 'or', '+', 'not', '+', '2', '+', '🐝'], 728 | words: ['To Be Or Not To Be'], 729 | hint: `A famous quote from Hamlet` 730 | }, 731 | { 732 | symbols: ['⬇', '➡', '+', '🤮'], 733 | words: ['downright disgusting'], 734 | hint: `Something extremely displeasing` 735 | }, 736 | { 737 | symbols: ['🔥', '+', '🤼', '+', 'er'], 738 | words: ['firefighter'], 739 | hint: 'They show up when there is a fire' 740 | }, 741 | { 742 | symbols: ['1', '+', 'ce', '+', 'n', '+', 'a', '+', '🔵', '+', '🌛'], 743 | words: ['Once', 'in', 'a', 'Blue Moon'], 744 | hint: 'Phrase used when something only happens once in a while' 745 | }, 746 | { 747 | symbols: ['🐱', '+', '🌲'], 748 | words: ['catalog'], 749 | hint: `The Sear's ________` 750 | }, 751 | { 752 | symbols: ['💋', '+', 'able'], 753 | words: ['kissable'], 754 | hint: `cute animals are very _____` 755 | }, 756 | { 757 | symbols: ['😄', '+', '🎂🎁'], 758 | words: ['Happy', 'Birthday'], 759 | hint: '____ to you!' 760 | }, 761 | { 762 | symbols: ['🔥', '+', '⚽'], 763 | words: ['Fireball'], 764 | hint: 'The sun is a big ________' 765 | }, 766 | { 767 | symbols: ['💎', '+', '⃝'], 768 | words: ['Diamond', 'ring'], 769 | hint: ['Things you exchange in weddings.'] 770 | }, 771 | { 772 | symbols: ['Black', '+', '🕳'], 773 | words: ['Black', 'hole'], 774 | hint: ['Final form of a star.'] 775 | }, 776 | { 777 | symbols: ['🐼', '-', 'DA', '+', '🍰'], 778 | words: ['Pancake'], 779 | hint: 'A delicious breakfast' 780 | }, 781 | { 782 | symbols: ['✝', '+', '🏹'], 783 | words: ['Crossbow'], 784 | hint: 'What an unskilled archer would use' 785 | }, 786 | { 787 | symbols: ['D', '+', '💡'], 788 | words: ['delight'], 789 | hint: 'To please someone' 790 | }, 791 | { 792 | symbols: ['🌲', '+', 'T'], 793 | words: ['treaty'], 794 | hint: 'an agreement between countries' 795 | }, 796 | { 797 | symbols: ['👹', '+', '🚚'], 798 | words: ['monster', 'truck'], 799 | hint: 'they have gigantic wheels!' 800 | }, 801 | { 802 | symbols: ['🔑', '+', '⛓️'], 803 | words: ['keychain'], 804 | hint: 'Something that holds on to one or more keys' 805 | }, 806 | { 807 | symbols: ['💁', '+', '🖌'], 808 | words: ['hairbrush'], 809 | hint: 'Tidy your hair with' 810 | }, 811 | { 812 | symbols: ['💻'], 813 | words: ['laptop'], 814 | hint: 'A portable computer' 815 | }, 816 | { 817 | symbols: ['⚠', '⬇', '🔋'], 818 | words: ['low', 'battery'], 819 | hint: 'You need to charge your device soon' 820 | }, 821 | { 822 | symbols: ['📰'], 823 | words: ['newspaper'], 824 | hint: 'Reading today's ______' 825 | }, 826 | { 827 | symbols: ['✉'], 828 | words: ['mail'], 829 | hint: '"You've got ____"' 830 | }, 831 | { 832 | symbols: ['🌮', '+', '🥗'], 833 | words: ['taco', 'salad'], 834 | hint: 'One food made up of the other food' 835 | }, 836 | { 837 | symbols: ['🍇', '-', '(🐒🐒🐒)', '+', '🐜'], 838 | words: ['grant'], 839 | hint: 'Break it up. Do the subtraction then add the addition to the end' 840 | }, 841 | { 842 | symbols: ['👁', '+', '📱'], 843 | words: ['iPhone'], 844 | hint: 'Steve Jobs' 845 | }, 846 | { 847 | symbols: ['→', '🦁', '👑'], 848 | words: ['the', 'lion', 'king'], 849 | hint: `Classic 90's coming of age film about a lion who must re-take his father's throne and become king of the jungle` 850 | }, 851 | { 852 | symbols: ['🚐', '+', 'Gogh'], 853 | words: ['van', 'Gogh'], 854 | hint: 'Famous Dutch painter.' 855 | }, 856 | { 857 | symbols: ['🖖'], 858 | words: ['Star', 'Trek'], 859 | hint: 'Famous salute from what TV show?' 860 | }, 861 | { 862 | symbols: ['🎁', '+', '🃏'], 863 | words: ['gift', 'card'], 864 | hint: 'You get one on your birthday' 865 | }, 866 | { 867 | symbols: ['🏹', '+', '👔'], 868 | words: ['bow', 'tie'], 869 | hint: ['An accessory that is often worn with a suit'] 870 | }, 871 | { 872 | symbols: ['🔎', '+', '🎉'], 873 | words: ['search', 'party'], 874 | hint: ["Let's find something together"] 875 | }, 876 | { 877 | symbols: ['🐛', '+', '🕳'], 878 | words: ['worm', 'hole'], 879 | hint: 'A passage through space creating a shortcut through time and space' 880 | }, 881 | { 882 | symbols: ['🌎', 'OF', '⚔', 'CRAFT'], 883 | words: ['World', 'of', 'Warcraft'], 884 | hint: ['Famous Blizzard online game'] 885 | }, 886 | { 887 | symbols: ['🍯', '+', '🐝'], 888 | words: ['honeybee'], 889 | hint: ['An insect that makes a sweet treat.'] 890 | }, 891 | { 892 | symbols: ['🧶'], 893 | words: ['yarn'], 894 | hint: ['A package manager similar to NPM'] 895 | }, 896 | { 897 | symbols: ['☕️', '+', '📄'], 898 | words: ['java', 'script'], 899 | hint: ['It makes the website interactive'] 900 | }, 901 | { 902 | symbols: ['🦀', '+', '🎂'], 903 | words: ['crab', 'cake'], 904 | hint: 'A delicious appetizer' 905 | } 906 | ]; 907 | 908 | export function isRebusAnswered(id) { 909 | const answeredRebuses = window.localStorage.getItem('answeredRebuses'); 910 | return !!answeredRebuses && JSON.parse(answeredRebuses).includes(id); 911 | } 912 | 913 | export function markRebusAsAnswered(id) { 914 | const answeredRebuses = window.localStorage.getItem('answeredRebuses'); 915 | if (!answeredRebuses) { 916 | window.localStorage.setItem('answeredRebuses', JSON.stringify([id])); 917 | } else { 918 | window.localStorage.setItem( 919 | 'answeredRebuses', 920 | JSON.stringify([...JSON.parse(answeredRebuses), id]) 921 | ); 922 | } 923 | } 924 | 925 | export function getRebuses() { 926 | return rebuses.map((rebus, index) => { 927 | const id = index + 1; 928 | const isAnswered = isRebusAnswered(id); 929 | const chars = rebus.words.join(''); 930 | return { 931 | id, 932 | ...rebus, 933 | input: isAnswered ? [...chars] : [...Array(chars.length)], 934 | isAnswered 935 | }; 936 | }); 937 | } 938 | -------------------------------------------------------------------------------- /src/js/store.js: -------------------------------------------------------------------------------- 1 | import { confetti } from 'dom-confetti'; 2 | import { createStore } from './mini'; 3 | import { getRebuses, markRebusAsAnswered } from './rebuses'; 4 | 5 | export const actionsCreators = { 6 | next: ({ current, rebuses }) => ({ 7 | current: current < rebuses.length - 1 ? current + 1 : 0, 8 | animation: 'flip-vertical-right', 9 | incorrectAnswerCount: 0 10 | }), 11 | prev: ({ current, rebuses }) => ({ 12 | current: current > 0 ? current - 1 : rebuses.length - 1, 13 | animation: 'flip-vertical-left', 14 | incorrectAnswerCount: 0 15 | }), 16 | setCurrent: ({ rebuses }, id) => { 17 | const index = rebuses.findIndex(rebus => rebus.id === id); 18 | if (index > 0) { 19 | return { current: index }; 20 | } 21 | return {}; 22 | }, 23 | setInput: ({ current, rebuses }, input, wordIndex, charIndex) => { 24 | const rebus = rebuses[current]; 25 | const previousWords = rebus.words.slice(0, wordIndex).join(''); 26 | const index = wordIndex > 0 ? previousWords.length + charIndex : charIndex; 27 | const updatedRebuses = [...rebuses]; 28 | updatedRebuses[current].input[index] = input; 29 | return { updatedRebuses }; 30 | }, 31 | check: ({ current, rebuses, incorrectAnswerCount }, confettiCanon) => { 32 | const rebus = rebuses[current]; 33 | const input = rebus.input.join('').toUpperCase(); 34 | const answer = rebus.words.join('').toUpperCase(); 35 | if (input.length !== answer.length) { 36 | return {}; 37 | } 38 | if (input === answer) { 39 | markRebusAsAnswered(rebus.id); 40 | confetti(confettiCanon); 41 | const updatedRebuses = [...rebuses]; 42 | updatedRebuses[current].isAnswered = true; 43 | return { updatedRebuses, animation: 'none', incorrectAnswerCount: 0 }; 44 | } 45 | return { incorrectAnswerCount: incorrectAnswerCount + 1 }; 46 | }, 47 | shake: ({ current, rebuses }) => { 48 | const rebus = rebuses[current]; 49 | const isAnswered = rebus.words.join('').toUpperCase() === rebus.input.join('').toUpperCase(); 50 | if (!isAnswered) { 51 | return { animation: 'shake' }; 52 | } 53 | return {}; 54 | } 55 | }; 56 | 57 | export const initialState = { 58 | current: 0, 59 | animation: 'none', 60 | rebuses: getRebuses(), 61 | incorrectAnswerCount: 0 62 | }; 63 | 64 | export const { connect, actions } = createStore(initialState, actionsCreators); 65 | -------------------------------------------------------------------------------- /tests/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /tests/__snapshots__/app.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Tests for app init renders the app into the root element 1`] = ` 4 | Object { 5 | "children": Array [ 6 | Object { 7 | "children": Array [], 8 | "componentDidMount": undefined, 9 | "componentDidUpdate": undefined, 10 | "props": Object {}, 11 | "render": [Function], 12 | "update": [Function], 13 | }, 14 | Object { 15 | "children": Array [], 16 | "componentDidMount": undefined, 17 | "componentDidUpdate": undefined, 18 | "props": Object { 19 | "url": "https://github.com/ollelauribostrom/rebus", 20 | }, 21 | "render": [Function], 22 | "update": [Function], 23 | }, 24 | Object { 25 | "children": Array [], 26 | "componentDidMount": undefined, 27 | "componentDidUpdate": undefined, 28 | "props": Object { 29 | "className": "change-button--prev", 30 | "onClick": [Function], 31 | }, 32 | "render": [Function], 33 | "update": [Function], 34 | }, 35 | Object { 36 | "children": Array [], 37 | "componentDidMount": [Function], 38 | "componentDidUpdate": [Function], 39 | "props": Object { 40 | "charInput": [Function], 41 | }, 42 | "render": [Function], 43 | "update": [Function], 44 | }, 45 | Object { 46 | "children": Array [], 47 | "componentDidMount": undefined, 48 | "componentDidUpdate": undefined, 49 | "props": Object { 50 | "className": "change-button--next", 51 | "onClick": [Function], 52 | }, 53 | "render": [Function], 54 | "update": [Function], 55 | }, 56 | Object { 57 | "children": Array [], 58 | "componentDidMount": undefined, 59 | "componentDidUpdate": undefined, 60 | "props": Object {}, 61 | "render": [Function], 62 | "update": [Function], 63 | }, 64 | Object { 65 | "children": Array [], 66 | "componentDidMount": undefined, 67 | "componentDidUpdate": undefined, 68 | "props": Object {}, 69 | "render": [Function], 70 | "update": [Function], 71 | }, 72 | ], 73 | "componentDidMount": undefined, 74 | "componentDidUpdate": undefined, 75 | "props": Object {}, 76 | "render": [Function], 77 | "update": [Function], 78 | } 79 | `; 80 | -------------------------------------------------------------------------------- /tests/__snapshots__/components.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Tests for components App renders correctly 1`] = ` 4 | Object { 5 | "children": Array [ 6 | Object { 7 | "children": Array [], 8 | "componentDidMount": undefined, 9 | "componentDidUpdate": undefined, 10 | "props": Object {}, 11 | "render": [Function], 12 | "update": [Function], 13 | }, 14 | ], 15 | "componentDidMount": undefined, 16 | "componentDidUpdate": undefined, 17 | "props": Object {}, 18 | "render": [Function], 19 | "update": [Function], 20 | } 21 | `; 22 | 23 | exports[`Tests for components App renders correctly 2`] = ` 24 | " 25 |
26 | 27 |
28 | " 29 | `; 30 | 31 | exports[`Tests for components ChangeButton renders correctly (with className prop) 1`] = ` 32 | Object { 33 | "children": Array [], 34 | "componentDidMount": undefined, 35 | "componentDidUpdate": undefined, 36 | "props": Object { 37 | "className": "test", 38 | }, 39 | "render": [Function], 40 | "update": [Function], 41 | } 42 | `; 43 | 44 | exports[`Tests for components ChangeButton renders correctly (with className prop) 2`] = ` 45 | " 46 | 60 | " 61 | `; 62 | 63 | exports[`Tests for components ChangeButton renders correctly (without className prop) 1`] = ` 64 | Object { 65 | "children": Array [], 66 | "componentDidMount": undefined, 67 | "componentDidUpdate": undefined, 68 | "props": Object {}, 69 | "render": [Function], 70 | "update": [Function], 71 | } 72 | `; 73 | 74 | exports[`Tests for components ChangeButton renders correctly (without className prop) 2`] = ` 75 | " 76 | 90 | " 91 | `; 92 | 93 | exports[`Tests for components Char renders correctly (with value and multiple words) 1`] = ` 94 | Object { 95 | "children": Array [], 96 | "componentDidMount": undefined, 97 | "componentDidUpdate": undefined, 98 | "props": Object { 99 | "animation": "none", 100 | "charIndex": 1, 101 | "current": 0, 102 | "incorrectAnswerCount": 0, 103 | "rebuses": Array [ 104 | Object { 105 | "hint": "hint", 106 | "input": Array [ 107 | undefined, 108 | undefined, 109 | undefined, 110 | undefined, 111 | "c", 112 | undefined, 113 | ], 114 | "isAnswered": false, 115 | "symbols": Array [ 116 | "😍", 117 | "👍", 118 | "😍", 119 | ], 120 | "words": Array [ 121 | "one", 122 | "two", 123 | ], 124 | }, 125 | ], 126 | "wordIndex": 1, 127 | }, 128 | "render": [Function], 129 | "update": [Function], 130 | } 131 | `; 132 | 133 | exports[`Tests for components Char renders correctly (with value and multiple words) 2`] = ` 134 | " 135 | " 143 | `; 144 | 145 | exports[`Tests for components Char renders correctly (with value and single word) 1`] = ` 146 | Object { 147 | "children": Array [], 148 | "componentDidMount": undefined, 149 | "componentDidUpdate": undefined, 150 | "props": Object { 151 | "animation": "none", 152 | "charIndex": 1, 153 | "current": 0, 154 | "incorrectAnswerCount": 0, 155 | "rebuses": Array [ 156 | Object { 157 | "hint": "hint", 158 | "input": Array [ 159 | undefined, 160 | "c", 161 | undefined, 162 | undefined, 163 | undefined, 164 | undefined, 165 | ], 166 | "isAnswered": false, 167 | "symbols": Array [ 168 | "😍", 169 | "👍", 170 | "😍", 171 | ], 172 | "words": Array [ 173 | "one", 174 | "two", 175 | ], 176 | }, 177 | ], 178 | "wordIndex": 0, 179 | }, 180 | "render": [Function], 181 | "update": [Function], 182 | } 183 | `; 184 | 185 | exports[`Tests for components Char renders correctly (with value and single word) 2`] = ` 186 | " 187 | " 195 | `; 196 | 197 | exports[`Tests for components Char renders correctly (without value) 1`] = ` 198 | Object { 199 | "children": Array [], 200 | "componentDidMount": undefined, 201 | "componentDidUpdate": undefined, 202 | "props": Object { 203 | "animation": "none", 204 | "charIndex": 1, 205 | "current": 0, 206 | "incorrectAnswerCount": 0, 207 | "rebuses": Array [ 208 | Object { 209 | "hint": "hint", 210 | "input": Array [ 211 | undefined, 212 | undefined, 213 | undefined, 214 | undefined, 215 | undefined, 216 | undefined, 217 | ], 218 | "isAnswered": false, 219 | "symbols": Array [ 220 | "😍", 221 | "👍", 222 | "😍", 223 | ], 224 | "words": Array [ 225 | "one", 226 | "two", 227 | ], 228 | }, 229 | ], 230 | "wordIndex": 0, 231 | }, 232 | "render": [Function], 233 | "update": [Function], 234 | } 235 | `; 236 | 237 | exports[`Tests for components Char renders correctly (without value) 2`] = ` 238 | " 239 | " 247 | `; 248 | 249 | exports[`Tests for components GithubCorner renders correctly 1`] = ` 250 | Object { 251 | "children": Array [], 252 | "componentDidMount": undefined, 253 | "componentDidUpdate": undefined, 254 | "props": Object { 255 | "url": "test", 256 | }, 257 | "render": [Function], 258 | "update": [Function], 259 | } 260 | `; 261 | 262 | exports[`Tests for components GithubCorner renders correctly 2`] = ` 263 | " 264 | 265 | 266 | 267 | 269 | 271 | 272 | 273 | " 274 | `; 275 | 276 | exports[`Tests for components Hint renders correctly (when incorrect answer count is less than max incorrect answer count) 1`] = ` 277 | Object { 278 | "children": Array [], 279 | "componentDidMount": undefined, 280 | "componentDidUpdate": undefined, 281 | "props": Object { 282 | "animation": "none", 283 | "current": 0, 284 | "incorrectAnswerCount": 1, 285 | "rebuses": Array [ 286 | Object { 287 | "hint": "hint", 288 | "input": Array [ 289 | undefined, 290 | undefined, 291 | undefined, 292 | undefined, 293 | undefined, 294 | undefined, 295 | ], 296 | "isAnswered": false, 297 | "symbols": Array [ 298 | "😍", 299 | "👍", 300 | "😍", 301 | ], 302 | "words": Array [ 303 | "one", 304 | "two", 305 | ], 306 | }, 307 | ], 308 | }, 309 | "render": [Function], 310 | "update": [Function], 311 | } 312 | `; 313 | 314 | exports[`Tests for components Hint renders correctly (when incorrect answer count is less than max incorrect answer count) 2`] = ` 315 | " 316 | 317 | " 318 | `; 319 | 320 | exports[`Tests for components Hint renders correctly (when incorrect answer count is more than max incorrect answer count) 1`] = ` 321 | Object { 322 | "children": Array [], 323 | "componentDidMount": undefined, 324 | "componentDidUpdate": undefined, 325 | "props": Object { 326 | "animation": "none", 327 | "current": 0, 328 | "incorrectAnswerCount": 4, 329 | "rebuses": Array [ 330 | Object { 331 | "hint": "hint", 332 | "input": Array [ 333 | undefined, 334 | undefined, 335 | undefined, 336 | undefined, 337 | undefined, 338 | undefined, 339 | ], 340 | "isAnswered": false, 341 | "symbols": Array [ 342 | "😍", 343 | "👍", 344 | "😍", 345 | ], 346 | "words": Array [ 347 | "one", 348 | "two", 349 | ], 350 | }, 351 | ], 352 | }, 353 | "render": [Function], 354 | "update": [Function], 355 | } 356 | `; 357 | 358 | exports[`Tests for components Hint renders correctly (when incorrect answer count is more than max incorrect answer count) 2`] = ` 359 | " 360 | 361 | 💡 hint 362 | 363 | " 364 | `; 365 | 366 | exports[`Tests for components Logo renders correctly 1`] = ` 367 | Object { 368 | "children": Array [], 369 | "componentDidMount": undefined, 370 | "componentDidUpdate": undefined, 371 | "props": Object {}, 372 | "render": [Function], 373 | "update": [Function], 374 | } 375 | `; 376 | 377 | exports[`Tests for components Logo renders correctly 2`] = ` 378 | " 379 | 381 | 382 | 383 | 384 | 386 | 387 | 388 | 389 | 390 | " 391 | `; 392 | 393 | exports[`Tests for components ProgressBar renders correctly (without rebuses) 1`] = ` 394 | Object { 395 | "children": Array [], 396 | "componentDidMount": undefined, 397 | "componentDidUpdate": undefined, 398 | "props": Object { 399 | "rebuses": Array [], 400 | }, 401 | "render": [Function], 402 | "update": [Function], 403 | } 404 | `; 405 | 406 | exports[`Tests for components ProgressBar renders correctly (without rebuses) 2`] = ` 407 | " 408 | 409 | 410 | " 411 | `; 412 | 413 | exports[`Tests for components ProgressBar renders correctly 1`] = ` 414 | Object { 415 | "children": Array [], 416 | "componentDidMount": undefined, 417 | "componentDidUpdate": undefined, 418 | "props": Object { 419 | "animation": "none", 420 | "current": 0, 421 | "incorrectAnswerCount": 0, 422 | "rebuses": Array [ 423 | Object { 424 | "hint": "hint", 425 | "input": Array [ 426 | undefined, 427 | undefined, 428 | undefined, 429 | undefined, 430 | undefined, 431 | undefined, 432 | ], 433 | "isAnswered": false, 434 | "symbols": Array [ 435 | "😍", 436 | "👍", 437 | "😍", 438 | ], 439 | "words": Array [ 440 | "one", 441 | "two", 442 | ], 443 | }, 444 | ], 445 | }, 446 | "render": [Function], 447 | "update": [Function], 448 | } 449 | `; 450 | 451 | exports[`Tests for components ProgressBar renders correctly 2`] = ` 452 | " 453 | 454 | 455 | " 456 | `; 457 | 458 | exports[`Tests for components Rebus renders correctly (when rebus is answered) 1`] = ` 459 | Object { 460 | "children": Array [], 461 | "componentDidMount": [Function], 462 | "componentDidUpdate": [Function], 463 | "props": Object { 464 | "animation": "none", 465 | "current": 0, 466 | "incorrectAnswerCount": 0, 467 | "rebuses": Array [ 468 | Object { 469 | "hint": "hint", 470 | "input": Array [ 471 | undefined, 472 | undefined, 473 | undefined, 474 | undefined, 475 | undefined, 476 | undefined, 477 | ], 478 | "isAnswered": true, 479 | "symbols": Array [ 480 | "😍", 481 | "👍", 482 | "😍", 483 | ], 484 | "words": Array [ 485 | "one", 486 | "two", 487 | ], 488 | }, 489 | ], 490 | }, 491 | "render": [Function], 492 | "update": [Function], 493 | } 494 | `; 495 | 496 | exports[`Tests for components Rebus renders correctly (when rebus is answered) 2`] = ` 497 | " 498 |
499 |
500 | 1/1 501 |
502 | 😍 👍 😍 503 |
504 | 505 |
506 |
507 | " 508 | `; 509 | 510 | exports[`Tests for components Rebus renders correctly (with animation class) 1`] = ` 511 | Object { 512 | "children": Array [], 513 | "componentDidMount": [Function], 514 | "componentDidUpdate": [Function], 515 | "props": Object { 516 | "animation": "flip-vertical-right", 517 | "current": 0, 518 | "incorrectAnswerCount": 0, 519 | "rebuses": Array [ 520 | Object { 521 | "hint": "hint", 522 | "input": Array [ 523 | undefined, 524 | undefined, 525 | undefined, 526 | undefined, 527 | undefined, 528 | undefined, 529 | ], 530 | "isAnswered": false, 531 | "symbols": Array [ 532 | "😍", 533 | "👍", 534 | "😍", 535 | ], 536 | "words": Array [ 537 | "one", 538 | "two", 539 | ], 540 | }, 541 | ], 542 | }, 543 | "render": [Function], 544 | "update": [Function], 545 | } 546 | `; 547 | 548 | exports[`Tests for components Rebus renders correctly (with animation class) 2`] = ` 549 | " 550 |
551 |
552 | 1/1 553 |
554 | 😍 👍 😍 555 |
556 | 557 |
558 |
559 | " 560 | `; 561 | 562 | exports[`Tests for components Rebus renders correctly 1`] = ` 563 | Object { 564 | "children": Array [], 565 | "componentDidMount": [Function], 566 | "componentDidUpdate": [Function], 567 | "props": Object { 568 | "animation": "none", 569 | "current": 0, 570 | "incorrectAnswerCount": 0, 571 | "rebuses": Array [ 572 | Object { 573 | "hint": "hint", 574 | "input": Array [ 575 | undefined, 576 | undefined, 577 | undefined, 578 | undefined, 579 | undefined, 580 | undefined, 581 | ], 582 | "isAnswered": false, 583 | "symbols": Array [ 584 | "😍", 585 | "👍", 586 | "😍", 587 | ], 588 | "words": Array [ 589 | "one", 590 | "two", 591 | ], 592 | }, 593 | ], 594 | }, 595 | "render": [Function], 596 | "update": [Function], 597 | } 598 | `; 599 | 600 | exports[`Tests for components Rebus renders correctly 2`] = ` 601 | " 602 |
603 |
604 | 1/1 605 |
606 | 😍 👍 😍 607 |
608 | 609 |
610 |
611 | " 612 | `; 613 | 614 | exports[`Tests for components Word renders correctly 1`] = ` 615 | Object { 616 | "children": Array [], 617 | "componentDidMount": undefined, 618 | "componentDidUpdate": undefined, 619 | "props": Object { 620 | "charInput": [MockFunction], 621 | "word": "one", 622 | "wordIndex": 0, 623 | }, 624 | "render": [Function], 625 | "update": [Function], 626 | } 627 | `; 628 | 629 | exports[`Tests for components Word renders correctly 2`] = ` 630 | " 631 |
632 | 633 |
634 | " 635 | `; 636 | -------------------------------------------------------------------------------- /tests/__snapshots__/mini.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Tests for mini framework render/component renders a component with children 1`] = ` 4 | "
5 | Child1Child2
" 6 | `; 7 | 8 | exports[`Tests for mini framework render/component renders a single component without children 1`] = `"Child1"`; 9 | 10 | exports[`Tests for mini framework store updates connected components on state change 1`] = ` 11 | "
12 | Not connected1
" 13 | `; 14 | -------------------------------------------------------------------------------- /tests/__snapshots__/store.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Tests for store should have the correct initial state 1`] = ` 4 | Object { 5 | "animation": "none", 6 | "current": 0, 7 | "incorrectAnswerCount": 0, 8 | "rebuses": Array [ 9 | Object { 10 | "id": 1, 11 | "input": Array [ 12 | undefined, 13 | undefined, 14 | undefined, 15 | undefined, 16 | undefined, 17 | undefined, 18 | ], 19 | "isAnswered": false, 20 | "symbols": Array [ 21 | "😍", 22 | "👍", 23 | "😍", 24 | ], 25 | "words": Array [ 26 | "one", 27 | "two", 28 | ], 29 | }, 30 | Object { 31 | "id": 2, 32 | "input": Array [ 33 | undefined, 34 | undefined, 35 | undefined, 36 | undefined, 37 | undefined, 38 | undefined, 39 | undefined, 40 | undefined, 41 | undefined, 42 | ], 43 | "isAnswered": false, 44 | "symbols": Array [ 45 | "😍", 46 | "👍", 47 | "😍", 48 | ], 49 | "words": Array [ 50 | "three", 51 | "four", 52 | ], 53 | }, 54 | ], 55 | } 56 | `; 57 | -------------------------------------------------------------------------------- /tests/app.spec.js: -------------------------------------------------------------------------------- 1 | import { init, registerListeners, setCurrentFromURL } from '../src/js/app'; 2 | import { actions as actionsMock } from '../src/js/store'; 3 | import * as renderMock from '../src/js/mini/render'; 4 | 5 | jest.mock('../src/js/mini/render', () => { 6 | const mock = {}; 7 | function reset() { 8 | Object.assign(mock, { 9 | render: jest.fn(), 10 | reset 11 | }); 12 | } 13 | reset(); 14 | return mock; 15 | }); 16 | 17 | jest.mock('../src/js/store', () => ({ 18 | actions: { 19 | next: jest.fn(), 20 | prev: jest.fn(), 21 | setCurrent: jest.fn() 22 | }, 23 | connect: component => component 24 | })); 25 | 26 | afterEach(() => { 27 | renderMock.reset(); 28 | }); 29 | 30 | const setup = () => { 31 | const container = document.createElement('div'); 32 | container.className = 'root'; 33 | document.body.append(container); 34 | return { container }; 35 | }; 36 | 37 | describe('Tests for app', () => { 38 | describe('init', () => { 39 | it('renders the app into the root element', () => { 40 | const { container } = setup(); 41 | init(); 42 | expect(renderMock.render).toHaveBeenCalledTimes(1); 43 | const firstCall = renderMock.render.mock.calls[0]; 44 | const [firstArg, secondArg] = firstCall; 45 | expect(firstArg).toMatchSnapshot(); 46 | expect(secondArg).toEqual(container); 47 | }); 48 | }); 49 | describe('registerListeners', () => { 50 | it('register a listener for keyup events', () => { 51 | const leftArrowEvent = new Event('keyup'); 52 | const rightArrowEvent = new Event('keyup'); 53 | leftArrowEvent.key = 'ArrowLeft'; 54 | rightArrowEvent.key = 'ArrowRight'; 55 | registerListeners(); 56 | document.dispatchEvent(leftArrowEvent); 57 | document.dispatchEvent(rightArrowEvent); 58 | expect(actionsMock.prev).toHaveBeenCalled(); 59 | expect(actionsMock.next).toHaveBeenCalled(); 60 | }); 61 | }); 62 | describe('setCurrentFromURL', () => { 63 | it('sets the current rebus based on the url query string', () => { 64 | window.history.pushState({}, 'Test', '/?rebus=2'); 65 | setCurrentFromURL(); 66 | expect(actionsMock.setCurrent).toHaveBeenCalledWith(2); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /tests/components.spec.js: -------------------------------------------------------------------------------- 1 | import { render } from '../src/js/mini'; 2 | import { App } from '../src/js/components/App'; 3 | import { Word } from '../src/js/components/Word'; 4 | import { Char } from '../src/js/components/Char'; 5 | import { Logo } from '../src/js/components/Logo'; 6 | import { GithubCorner } from '../src/js/components/GithubCorner'; 7 | import { ChangeButton } from '../src/js/components/ChangeButton'; 8 | import { Rebus } from '../src/js/components/Rebus'; 9 | import { ProgressBar } from '../src/js/components/ProgressBar'; 10 | import { Hint } from '../src/js/components/Hint'; 11 | 12 | jest.mock('../src/js/store', () => ({ 13 | connect: arg => arg 14 | })); 15 | 16 | function getMockState() { 17 | return { 18 | current: 0, 19 | animation: 'none', 20 | incorrectAnswerCount: 0, 21 | rebuses: [ 22 | { 23 | symbols: ['😍', '👍', '😍'], 24 | words: ['one', 'two'], 25 | hint: 'hint', 26 | input: [...Array(6)], 27 | isAnswered: false 28 | } 29 | ] 30 | }; 31 | } 32 | 33 | describe('Tests for components', () => { 34 | describe('App', () => { 35 | it('renders correctly', () => { 36 | const wrapper = App(Logo()); 37 | expect(wrapper).toMatchSnapshot(); 38 | expect(wrapper.render()).toMatchSnapshot(); 39 | }); 40 | }); 41 | describe('Rebus', () => { 42 | it('renders correctly', () => { 43 | const props = getMockState(); 44 | const wrapper = Rebus(props); 45 | expect(wrapper).toMatchSnapshot(); 46 | expect(wrapper.render(props)).toMatchSnapshot(); 47 | }); 48 | it('renders correctly (when rebus is answered)', () => { 49 | const props = getMockState(); 50 | props.rebuses[0].isAnswered = true; 51 | const wrapper = Rebus(props); 52 | expect(wrapper).toMatchSnapshot(); 53 | expect(wrapper.render(props)).toMatchSnapshot(); 54 | }); 55 | it('renders correctly (with animation class)', () => { 56 | const props = { ...getMockState(), animation: 'flip-vertical-right' }; 57 | const wrapper = Rebus(props); 58 | expect(wrapper).toMatchSnapshot(); 59 | expect(wrapper.render(props)).toMatchSnapshot(); 60 | }); 61 | }); 62 | describe('Word', () => { 63 | it('renders correctly', () => { 64 | const props = { word: 'one', wordIndex: 0, charInput: jest.fn() }; 65 | const wrapper = Word(props); 66 | expect(wrapper).toMatchSnapshot(); 67 | expect(wrapper.render(props)).toMatchSnapshot(); 68 | }); 69 | it('correctly registers onInput handlers for each char', () => { 70 | const onInput = jest.fn(); 71 | const props = { ...getMockState(), word: 'one', wordIndex: 0, charInput: onInput }; 72 | const root = document.createElement('div'); 73 | render(Word(props), root); 74 | const inputElement = root.querySelector('input'); 75 | inputElement.value = 'T'; 76 | inputElement.dispatchEvent(new Event('input')); 77 | expect(onInput).toHaveBeenCalledWith('T', 0, 0); 78 | }); 79 | it('jumps to next input field when a letter is pressed', () => { 80 | const onInput = jest.fn(); 81 | const props = { ...getMockState(), word: 'one', wordIndex: 0, charInput: onInput }; 82 | const root = document.createElement('div'); 83 | render(Word(props), root); 84 | const inputs = root.querySelectorAll('input'); 85 | inputs[0].focus(); 86 | inputs[0].value = 'T'; 87 | inputs[0].dispatchEvent(new Event('input')); 88 | expect(inputs[1] === document.activeElement).toEqual(true); 89 | }); 90 | it('remains on the same input field when a character that is not a letter is pressed', () => { 91 | const onInput = jest.fn(); 92 | const props = { ...getMockState(), word: 'one', wordIndex: 0, charInput: onInput }; 93 | const root = document.createElement('div'); 94 | render(Word(props), root); 95 | const inputs = root.querySelectorAll('input'); 96 | inputs[0].focus(); 97 | inputs[0].value = '!'; 98 | inputs[0].dispatchEvent(new Event('input')); 99 | expect(inputs[0] === document.activeElement).toEqual(true); 100 | }); 101 | it('jumps to the previous input field when pressing backspace in empty field', () => { 102 | const onInput = jest.fn(); 103 | const props = { ...getMockState(), word: 'one', wordIndex: 0, charInput: onInput }; 104 | const root = document.createElement('div'); 105 | render(Word(props), root); 106 | const inputs = root.querySelectorAll('input'); 107 | const mockEvent = new Event('keydown'); 108 | mockEvent.key = 'Backspace'; 109 | inputs[1].focus(); 110 | inputs[1].dispatchEvent(mockEvent); 111 | expect(inputs[0] === document.activeElement).toEqual(true); 112 | }); 113 | it('remains on the same input field when pressing backspace in non empty field', () => { 114 | const onInput = jest.fn(); 115 | const props = { ...getMockState(), word: 'one', wordIndex: 0, charInput: onInput }; 116 | const root = document.createElement('div'); 117 | render(Word(props), root); 118 | const inputs = root.querySelectorAll('input'); 119 | const mockEvent = new Event('keydown'); 120 | mockEvent.key = 'Backspace'; 121 | inputs[1].focus(); 122 | inputs[1].value = 'T'; 123 | inputs[1].dispatchEvent(mockEvent); 124 | expect(inputs[1] === document.activeElement).toEqual(true); 125 | }); 126 | it('clears field when user enters a valid letter character in non empty field', () => { 127 | const onInput = jest.fn(); 128 | const props = { ...getMockState(), word: 'one', wordIndex: 0, charInput: onInput }; 129 | const root = document.createElement('div'); 130 | render(Word(props), root); 131 | const inputs = root.querySelectorAll('input'); 132 | const mockEvent = new Event('keydown'); 133 | mockEvent.keyCode = '87'; 134 | inputs[1].focus(); 135 | inputs[1].value = 'T'; 136 | inputs[1].dispatchEvent(mockEvent); 137 | expect(inputs[1].value === '').toEqual(true); 138 | }); 139 | it('does not clear field if non letter character is entered', () => { 140 | const onInput = jest.fn(); 141 | const props = { ...getMockState(), word: 'one', wordIndex: 0, charInput: onInput }; 142 | const root = document.createElement('div'); 143 | render(Word(props), root); 144 | const inputs = root.querySelectorAll('input'); 145 | const mockEvent = new Event('keydown'); 146 | mockEvent.keyCode = '16'; 147 | inputs[1].focus(); 148 | inputs[1].value = 'T'; 149 | inputs[1].dispatchEvent(mockEvent); 150 | expect(inputs[1].value === 'T').toEqual(true); 151 | }); 152 | }); 153 | describe('Char', () => { 154 | it('renders correctly (without value)', () => { 155 | const props = { ...getMockState(), wordIndex: 0, charIndex: 1 }; 156 | const wrapper = Char(props); 157 | expect(wrapper).toMatchSnapshot(); 158 | expect(wrapper.render(props)).toMatchSnapshot(); 159 | }); 160 | it('renders correctly (with value and single word)', () => { 161 | const props = { ...getMockState(), wordIndex: 0, charIndex: 1 }; 162 | props.rebuses[0].input[1] = 'c'; 163 | const wrapper = Char(props); 164 | expect(wrapper).toMatchSnapshot(); 165 | expect(wrapper.render(props)).toMatchSnapshot(); 166 | }); 167 | it('renders correctly (with value and multiple words)', () => { 168 | const props = { ...getMockState(), wordIndex: 1, charIndex: 1 }; 169 | props.rebuses[0].input[4] = 'c'; 170 | const wrapper = Char(props); 171 | expect(wrapper).toMatchSnapshot(); 172 | expect(wrapper.render(props)).toMatchSnapshot(); 173 | }); 174 | it('disables inputs when rebus is answered', () => { 175 | const props = getMockState(); 176 | props.rebuses[0].isAnswered = true; 177 | const root = document.createElement('div'); 178 | render(Char(props), root); 179 | const input = root.querySelector('input'); 180 | expect(input.disabled).toEqual(true); 181 | }); 182 | }); 183 | describe('ChangeButton', () => { 184 | it('renders correctly (without className prop)', () => { 185 | const props = {}; 186 | const wrapper = ChangeButton(props); 187 | expect(wrapper).toMatchSnapshot(); 188 | expect(wrapper.render(props)).toMatchSnapshot(); 189 | }); 190 | it('renders correctly (with className prop)', () => { 191 | const props = { className: 'test' }; 192 | const wrapper = ChangeButton(props); 193 | expect(wrapper).toMatchSnapshot(); 194 | expect(wrapper.render(props)).toMatchSnapshot(); 195 | }); 196 | }); 197 | describe('GithubCorner', () => { 198 | it('renders correctly', () => { 199 | const props = { url: 'test' }; 200 | const wrapper = GithubCorner(props); 201 | expect(wrapper).toMatchSnapshot(); 202 | expect(wrapper.render(props)).toMatchSnapshot(); 203 | }); 204 | }); 205 | describe('Logo', () => { 206 | it('renders correctly', () => { 207 | const wrapper = Logo(); 208 | expect(wrapper).toMatchSnapshot(); 209 | expect(wrapper.render()).toMatchSnapshot(); 210 | }); 211 | }); 212 | describe('ProgressBar', () => { 213 | it('renders correctly', () => { 214 | const props = getMockState(); 215 | const wrapper = ProgressBar(props); 216 | expect(wrapper).toMatchSnapshot(); 217 | expect(wrapper.render(props)).toMatchSnapshot(); 218 | }); 219 | }); 220 | describe('ProgressBar', () => { 221 | it('renders correctly (without rebuses)', () => { 222 | const props = { rebuses: [] }; 223 | const wrapper = ProgressBar(props); 224 | expect(wrapper).toMatchSnapshot(); 225 | expect(wrapper.render(props)).toMatchSnapshot(); 226 | }); 227 | }); 228 | describe('Hint', () => { 229 | it('renders correctly (when incorrect answer count is more than max incorrect answer count)', () => { 230 | const props = { ...getMockState(), incorrectAnswerCount: 4 }; 231 | const wrapper = Hint(props); 232 | expect(wrapper).toMatchSnapshot(); 233 | expect(wrapper.render(props)).toMatchSnapshot(); 234 | }); 235 | }); 236 | describe('Hint', () => { 237 | it('renders correctly (when incorrect answer count is less than max incorrect answer count)', () => { 238 | const props = { ...getMockState(), incorrectAnswerCount: 1 }; 239 | const wrapper = Hint(props); 240 | expect(wrapper).toMatchSnapshot(); 241 | expect(wrapper.render(props)).toMatchSnapshot(); 242 | }); 243 | }); 244 | }); 245 | -------------------------------------------------------------------------------- /tests/mini.spec.js: -------------------------------------------------------------------------------- 1 | import { createComponent, render, createStore } from '../src/js/mini'; 2 | 3 | const wait = duration => new Promise(resolve => setTimeout(resolve, duration)); 4 | 5 | function Parent(...children) { 6 | return createComponent({ 7 | children, 8 | componentDidMount: jest.fn(), 9 | render() { 10 | return ` 11 |
12 | 13 |
`; 14 | } 15 | }); 16 | } 17 | 18 | function Child(props, componentDidMount) { 19 | return createComponent({ 20 | props, 21 | componentDidMount, 22 | render({ text }) { 23 | return `${text}`; 24 | } 25 | }); 26 | } 27 | 28 | describe('Tests for mini framework', () => { 29 | describe('render/component', () => { 30 | it('renders a single component without children', () => { 31 | const root = document.createElement('div'); 32 | render(Child({ text: 'Child1' }), root); 33 | expect(root.innerHTML).toMatchSnapshot(); 34 | }); 35 | it('renders a component with children', () => { 36 | const root = document.createElement('div'); 37 | render(Parent(Child({ text: 'Child1' }), Child({ text: 'Child2' })), root); 38 | expect(root.innerHTML).toMatchSnapshot(); 39 | }); 40 | it('registers event listeners passed as props', () => { 41 | const onClick = jest.fn(); 42 | const root = document.createElement('div'); 43 | render(Child({ onClick }), root); 44 | root.firstElementChild.click(); 45 | expect(onClick).toHaveBeenCalled(); 46 | }); 47 | it('calls componentDidMount after the component has been rendered', () => { 48 | const childOneDidMount = jest.fn(); 49 | const childTwoDidMount = jest.fn(); 50 | const root = document.createElement('div'); 51 | const parent = Parent( 52 | Child({ text: 'Child1' }, childOneDidMount), 53 | Child({ text: 'Child2' }, childTwoDidMount) 54 | ); 55 | render(parent, root); 56 | expect(parent.componentDidMount).toHaveBeenCalledTimes(1); 57 | expect(childOneDidMount).toHaveBeenCalledTimes(1); 58 | expect(childTwoDidMount).toHaveBeenCalledTimes(1); 59 | }); 60 | }); 61 | describe('store', () => { 62 | it('updates connected components on state change', async () => { 63 | const actionsCreators = { increment: ({ count }) => ({ count: count + 1 }) }; 64 | const initialState = { count: 0 }; 65 | const { connect, actions } = createStore(initialState, actionsCreators); 66 | function ConnectedChild(props) { 67 | return connect( 68 | createComponent({ 69 | props, 70 | componentDidUpdate: jest.fn(), 71 | render({ count }) { 72 | return `${count}`; 73 | } 74 | }) 75 | ); 76 | } 77 | const root = document.createElement('div'); 78 | const connectedChild = ConnectedChild(); 79 | render(Parent(Child({ text: 'Not connected' }), connectedChild), root); 80 | actions.increment(); 81 | await wait(30); 82 | expect(root.innerHTML).toMatchSnapshot(); 83 | expect(connectedChild.componentDidUpdate).toHaveBeenCalledTimes(1); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /tests/rebus.spec.js: -------------------------------------------------------------------------------- 1 | import { getRebuses, isRebusAnswered, markRebusAsAnswered } from '../src/js/rebuses'; 2 | 3 | describe('Tests for rebuses', () => { 4 | describe('getRebuses', () => { 5 | it('generates a correct array of rebuses', () => { 6 | const storage = jest.spyOn(Storage.prototype, 'getItem'); 7 | storage.mockImplementation(() => JSON.stringify([])); 8 | getRebuses().forEach(rebus => { 9 | expect(typeof rebus.id).toEqual('number'); 10 | expect(typeof rebus.symbols[0]).toEqual('string'); 11 | expect(typeof rebus.words[0]).toEqual('string'); 12 | expect(rebus.input.length).toEqual(rebus.words.join('').length); 13 | expect(rebus.isAnswered).toEqual(false); 14 | }); 15 | storage.mockRestore(); 16 | }); 17 | it('correctly restores answered rebuses', () => { 18 | const storage = jest.spyOn(Storage.prototype, 'getItem'); 19 | storage.mockImplementation(() => JSON.stringify([1])); 20 | const [rebus] = getRebuses(); 21 | expect(rebus.id).toEqual(1); 22 | expect(rebus.isAnswered).toEqual(true); 23 | expect(rebus.input).toEqual(['R', 'e', 'b', 'u', 's']); 24 | storage.mockRestore(); 25 | }); 26 | }); 27 | describe('isRebusAnswered', () => { 28 | it('returns true if the provided id has been marked as answered in localStorage', () => { 29 | const storage = jest.spyOn(Storage.prototype, 'getItem'); 30 | storage.mockImplementation(() => JSON.stringify([1])); 31 | expect(isRebusAnswered(1)).toEqual(true); 32 | expect(storage).toHaveBeenCalledWith('answeredRebuses'); 33 | expect(storage).toHaveBeenCalledTimes(1); 34 | storage.mockRestore(); 35 | }); 36 | it('returns false if the provided id has not been marked as answered in localStorage', () => { 37 | const storage = jest.spyOn(Storage.prototype, 'getItem'); 38 | storage.mockImplementation(() => JSON.stringify([1])); 39 | expect(isRebusAnswered(2)).toEqual(false); 40 | storage.mockRestore(); 41 | }); 42 | it('returns false if the key answeredRebuses has not yet been set in localStorage', () => { 43 | const storage = jest.spyOn(Storage.prototype, 'getItem'); 44 | storage.mockImplementation(() => null); 45 | expect(isRebusAnswered(2)).toEqual(false); 46 | storage.mockRestore(); 47 | }); 48 | }); 49 | describe('markRebusAsAnswered', () => { 50 | it('saves the provided rebus id to localStorage', () => { 51 | const storage = {}; 52 | const getItem = jest.spyOn(Storage.prototype, 'getItem'); 53 | const setItem = jest.spyOn(Storage.prototype, 'setItem'); 54 | getItem.mockImplementation(key => storage[key]); 55 | setItem.mockImplementation((key, value) => Object.assign(storage, { [key]: value })); 56 | markRebusAsAnswered(1); 57 | markRebusAsAnswered(2); 58 | expect(storage.answeredRebuses).toEqual('[1,2]'); 59 | }); 60 | }); 61 | describe('checkForRebusDuplicates', () => { 62 | it('checks for duplicate rebuses', () => { 63 | const rebuses = getRebuses(); 64 | const duplicates = []; 65 | rebuses.forEach(rebus => { 66 | rebuses.forEach(potentialDuplicate => { 67 | if (rebus !== potentialDuplicate && !duplicates.includes(rebus)) { 68 | if ( 69 | rebus.words.toString().toLowerCase() === 70 | potentialDuplicate.words.toString().toLowerCase() 71 | ) { 72 | duplicates.push(potentialDuplicate); 73 | } 74 | } 75 | }); 76 | }); 77 | expect(duplicates).toEqual([]); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /tests/store.spec.js: -------------------------------------------------------------------------------- 1 | import * as confetti from 'dom-confetti'; 2 | import { actionsCreators, initialState } from '../src/js/store'; 3 | 4 | jest.mock('../src/js/rebuses', () => ({ 5 | getRebuses: () => [ 6 | { 7 | id: 1, 8 | symbols: ['😍', '👍', '😍'], 9 | words: ['one', 'two'], 10 | input: [...Array(6)], 11 | isAnswered: false 12 | }, 13 | { 14 | id: 2, 15 | symbols: ['😍', '👍', '😍'], 16 | words: ['three', 'four'], 17 | input: [...Array(9)], 18 | isAnswered: false 19 | } 20 | ], 21 | markRebusAsAnswered: jest.fn() 22 | })); 23 | 24 | describe('Tests for store', () => { 25 | it('should have the correct initial state', () => { 26 | expect(initialState).toMatchSnapshot(); 27 | }); 28 | it('should handle action: next', () => { 29 | const state = { ...initialState }; 30 | const newState = Object.assign({}, state, actionsCreators.next(state)); 31 | const finalState = Object.assign({}, newState, actionsCreators.next(newState)); 32 | expect(newState.current).toEqual(1); 33 | expect(newState.incorrectAnswerCount).toEqual(0); 34 | expect(newState.animation).toEqual('flip-vertical-right'); 35 | expect(finalState.current).toEqual(0); 36 | expect(finalState.incorrectAnswerCount).toEqual(0); 37 | }); 38 | it('should handle action: prev', () => { 39 | const state = { ...initialState }; 40 | const newState = Object.assign({}, state, actionsCreators.prev(state)); 41 | const finalState = Object.assign({}, newState, actionsCreators.prev(newState)); 42 | expect(newState.current).toEqual(1); 43 | expect(newState.incorrectAnswerCount).toEqual(0); 44 | expect(newState.animation).toEqual('flip-vertical-left'); 45 | expect(finalState.current).toEqual(0); 46 | expect(finalState.incorrectAnswerCount).toEqual(0); 47 | }); 48 | it('should handle action: shake when isAnswered is false', () => { 49 | const state = { ...initialState }; 50 | const newState = Object.assign({}, state, actionsCreators.shake(state)); 51 | const finalState = Object.assign({}, newState, actionsCreators.shake(newState)); 52 | expect(state.animation).toEqual('none'); 53 | expect(newState.current).toEqual(0); 54 | expect(newState.animation).toEqual('shake'); 55 | expect(finalState.current).toEqual(0); 56 | expect(finalState.rebuses[0].isAnswered).toEqual(false); 57 | }); 58 | it('should handle action: setInput', () => { 59 | const state = { ...initialState }; 60 | const newState = Object.assign({}, state, actionsCreators.setInput(state, 'a', 0, 0)); 61 | const finalState = Object.assign({}, newState, actionsCreators.setInput(newState, 'b', 1, 0)); 62 | expect(finalState.animation).toEqual('none'); 63 | expect(finalState.rebuses[0].input).toEqual([ 64 | 'a', 65 | undefined, 66 | undefined, 67 | 'b', 68 | undefined, 69 | undefined 70 | ]); 71 | }); 72 | it('should handle action: check', () => { 73 | const confettiCanon = document.createElement('div'); 74 | const confettiSpy = jest.spyOn(confetti, 'confetti'); 75 | const state = { ...initialState }; 76 | const newState = Object.assign({}, state, actionsCreators.check(state, confettiCanon)); 77 | expect(newState.rebuses[0].isAnswered).toEqual(false); 78 | expect(newState.incorrectAnswerCount).toEqual(0); 79 | newState.rebuses[0].input = ['O', 'n', 'E', 'T', 'w', 'U']; 80 | const nextState = Object.assign({}, newState, actionsCreators.check(newState, confettiCanon)); 81 | expect(nextState.incorrectAnswerCount).toEqual(1); 82 | expect(nextState.rebuses[0].isAnswered).toEqual(false); 83 | newState.rebuses[0].input = ['O', 'n', 'E', 'T', 'w', 'O']; 84 | const finalState = Object.assign({}, newState, actionsCreators.check(newState, confettiCanon)); 85 | expect(finalState.rebuses[0].isAnswered).toEqual(true); 86 | expect(finalState.incorrectAnswerCount).toEqual(0); 87 | expect(confettiSpy).toHaveBeenCalledTimes(1); 88 | expect(confettiSpy).toHaveBeenCalledWith(confettiCanon); 89 | }); 90 | it('should handle action: setCurrent', () => { 91 | const state = { ...initialState }; 92 | const newState = Object.assign({}, state, actionsCreators.setCurrent(state, 2)); 93 | const finalState = Object.assign({}, newState, actionsCreators.setCurrent(newState, 9999)); 94 | expect(finalState.current).toEqual(1); 95 | }); 96 | it('should handle action: shake when isAnswered is true', () => { 97 | const confettiCanon = document.createElement('div'); 98 | const confettiSpy = jest.spyOn(confetti, 'confetti'); 99 | const state = { ...initialState }; 100 | const newState = Object.assign({}, state, actionsCreators.check(state, confettiCanon)); 101 | newState.rebuses[0].input = ['O', 'n', 'E', 'T', 'w', 'O']; 102 | const finalState = Object.assign({}, newState, actionsCreators.check(newState, confettiCanon)); 103 | expect(finalState.rebuses[0].isAnswered).toEqual(true); 104 | expect(confettiSpy).toHaveBeenCalledWith(confettiCanon); 105 | const stateShake = { ...initialState }; 106 | const newStateShake = Object.assign({}, stateShake, actionsCreators.shake(stateShake)); 107 | const finalStateShake = Object.assign({}, newStateShake, actionsCreators.shake(newStateShake)); 108 | expect(stateShake.animation).toEqual('none'); 109 | expect(newStateShake.current).toEqual(0); 110 | expect(newStateShake.animation).toEqual('none'); 111 | expect(finalStateShake.current).toEqual(0); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebPackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = publicPath => ({ 5 | entry: { 6 | main: './src/js/app.js' 7 | }, 8 | output: { 9 | filename: '[name].bundle.js', 10 | chunkFilename: '[name].bundle.js', 11 | path: path.resolve(__dirname, 'dist/'), 12 | publicPath: publicPath || '/' 13 | }, 14 | devtool: 'source-map', 15 | resolve: { extensions: ['.js', '.json'] }, 16 | module: { 17 | rules: [ 18 | // Process JS with Babel. 19 | { 20 | test: /\.(js)$/, 21 | include: path.resolve(__dirname, 'src/'), 22 | loader: require.resolve('babel-loader') 23 | }, 24 | // HTML 25 | { test: /\.html$/, use: ['html-loader'] }, 26 | // CSS 27 | { test: /\.css$/, use: ['style-loader', 'css-loader'] } 28 | ] 29 | }, 30 | plugins: [ 31 | new HtmlWebPackPlugin({ 32 | template: './src/index.html', 33 | favicon: './src/favicon.ico', 34 | filename: 'index.html' 35 | }) 36 | ], 37 | devServer: { 38 | contentBase: path.join(__dirname, 'dist'), 39 | port: 3000 40 | } 41 | }); 42 | --------------------------------------------------------------------------------