├── .babelrc ├── .editorconfig ├── .eslintignore ├── .github └── quotion.png ├── .gitignore ├── .travis.yml ├── .vscode ├── cSpell.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── README_CN.md ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── polyfills.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── webpackDevServer.config.js ├── internals ├── img │ ├── eslint-padded-90.png │ ├── jest-padded-90.png │ ├── react-padded-90.png │ ├── react-router-padded-90.png │ ├── redux-padded-90.png │ ├── redux-saga-padded-90.png │ ├── webpack-padded-90.png │ └── yarn-padded-90.png └── screenshots │ ├── comments.gif │ ├── data-persist.gif │ ├── i18n.png │ ├── iPhone.png │ ├── pwa.gif │ ├── redux-state.gif │ ├── reponsive.gif │ └── web.png ├── package.json ├── public ├── favicon-maskable.png ├── favicon.ico ├── favicon.png ├── index.html ├── manifest.json └── sitemap.xml ├── renovate.json ├── scripts ├── build.js ├── deploy.sh ├── start.js └── test.js ├── src ├── apis │ └── index.js ├── assets │ ├── audio │ │ ├── move.mp3 │ │ └── popup.mp3 │ ├── images │ │ ├── github-white.png │ │ └── github.png │ ├── styles │ │ ├── index.scss │ │ ├── normalize.scss │ │ └── vars.scss │ └── svg │ │ ├── arrow.svg │ │ ├── reset.svg │ │ ├── speaker-off.svg │ │ ├── speaker-on.svg │ │ └── undo.svg ├── components │ ├── Board │ │ ├── __tests__ │ │ │ ├── Board.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Board.test.js.snap │ │ ├── board.scss │ │ └── index.js │ ├── Button │ │ ├── __tests__ │ │ │ ├── Button.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Button.test.js.snap │ │ ├── button.scss │ │ └── index.js │ ├── Cell │ │ ├── __tests__ │ │ │ ├── Cell.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Cell.test.js.snap │ │ ├── cell.scss │ │ └── index.js │ ├── Comments │ │ ├── __tests__ │ │ │ ├── Comments.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Comments.test.js.snap │ │ ├── comments.scss │ │ └── index.js │ ├── Firework │ │ ├── __tests__ │ │ │ ├── Firework.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Firework.test.js.snap │ │ ├── firework.scss │ │ └── index.js │ ├── Footer │ │ ├── __tests__ │ │ │ ├── Footer.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Footer.test.js.snap │ │ ├── footer.scss │ │ └── index.js │ ├── Modal │ │ ├── __tests__ │ │ │ ├── Modal.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Modal.test.js.snap │ │ ├── index.js │ │ └── modal.scss │ ├── Row │ │ ├── __tests__ │ │ │ ├── Row.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Row.test.js.snap │ │ └── index.js │ ├── Scores │ │ ├── __tests__ │ │ │ ├── Scores.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Scores.test.js.snap │ │ ├── index.js │ │ └── scores.scss │ ├── Speaker │ │ ├── __tests__ │ │ │ ├── Speaker.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Speaker.test.js.snap │ │ └── index.js │ ├── Tips │ │ ├── __tests__ │ │ │ ├── Tips.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Tips.test.js.snap │ │ ├── index.js │ │ └── tips.scss │ └── WrapperButton │ │ ├── __tests__ │ │ ├── WrapperButton.test.js │ │ └── __snapshots__ │ │ │ └── WrapperButton.test.js.snap │ │ ├── index.js │ │ └── wrapperButton.scss ├── containers │ ├── App │ │ ├── App.js │ │ ├── __tests__ │ │ │ ├── App.test.js │ │ │ └── __snapshots__ │ │ │ │ └── App.test.js.snap │ │ └── index.js │ ├── ControlPanel │ │ ├── ControlPanel.js │ │ ├── __tests__ │ │ │ ├── ControlPanel.test.js │ │ │ └── __snapshots__ │ │ │ │ └── ControlPanel.test.js.snap │ │ ├── controlPanel.scss │ │ └── index.js │ ├── GameOver │ │ ├── __tests__ │ │ │ ├── GameOver.test.js │ │ │ └── __snapshots__ │ │ │ │ └── GameOver.test.js.snap │ │ ├── gameOver.scss │ │ └── index.js │ ├── MobileApp │ │ ├── MobileApp.js │ │ ├── __tests__ │ │ │ ├── MobileApp.test.js │ │ │ └── __snapshots__ │ │ │ │ └── MobileApp.test.js.snap │ │ ├── index.js │ │ └── mobileApp.scss │ ├── Ranking │ │ ├── __tests__ │ │ │ ├── Ranking.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Ranking.test.js.snap │ │ ├── index.js │ │ └── ranking.scss │ └── WebApp │ │ ├── WebApp.js │ │ ├── __tests__ │ │ ├── WebApp.test.js │ │ └── __snapshots__ │ │ │ └── WebApp.test.js.snap │ │ ├── index.js │ │ └── webApp.scss ├── index.js ├── layouts │ ├── Header │ │ ├── __tests__ │ │ │ ├── Header.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Header.test.js.snap │ │ ├── header.scss │ │ └── index.js │ ├── Main │ │ ├── __tests__ │ │ │ ├── Main.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Main.test.js.snap │ │ └── index.js │ ├── __tests__ │ │ ├── Layout.test.js │ │ └── __snapshots__ │ │ │ └── Layout.test.js.snap │ └── index.js ├── reducers │ ├── __tests__ │ │ └── board.test.js │ ├── board.js │ ├── index.js │ └── ranking.js ├── sagas │ └── index.js ├── store.js └── utils │ ├── __tests__ │ ├── mobileEvents.test.js │ └── registerServiceWorker.test.js │ ├── gitalk.js │ ├── helpers.js │ ├── i18n.js │ ├── mobileEvents.js │ └── registerServiceWorker.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "react", 10 | "stage-2" 11 | ], 12 | "env": { 13 | "test": { 14 | "presets": [ 15 | "env", 16 | "stage-2" 17 | ], 18 | "sourceMaps": "inline" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /config/ 2 | /scripts/ 3 | /public/ 4 | /build/ 5 | /coverage/ 6 | -------------------------------------------------------------------------------- /.github/quotion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devrsi0n/React-2048-game/d348adece13e466a35bdfd37acf45486d39beec0/.github/quotion.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | /.idea 24 | /profiles 25 | /monitor 26 | /.vscode/chrome 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | install: 5 | - npm i -g codecov 6 | - yarn 7 | script: 8 | - yarn run ci 9 | - yarn run build 10 | after_script: 11 | - yarn run codecov 12 | -------------------------------------------------------------------------------- /.vscode/cSpell.json: -------------------------------------------------------------------------------- 1 | { 2 | // cSpell Settings 3 | // cSpell Settings 4 | // Version of the setting file. Always 0.1 5 | "version": "0.1", 6 | // language - current active spelling language 7 | "language": "en", 8 | // words - list of words to be always considered correct 9 | "words": [ 10 | "codecov", 11 | "gitment", 12 | "heroku", 13 | "onstatechange", 14 | "onupdatefound", 15 | "Unmount", 16 | "WASD", 17 | "xmlns" 18 | ], 19 | // flagWords - list of words to be always considered incorrect 20 | // This is useful for offensive words and common spelling errors. 21 | // For example "hte" should be "the" 22 | "flagWords": ["hte"] 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [{ 7 | "name": "Chrome", 8 | "type": "chrome", 9 | "request": "launch", 10 | "url": "http://localhost:3000", 11 | "webRoot": "${workspaceRoot}/src", 12 | "userDataDir": "${workspaceRoot}/.vscode/chrome", 13 | "sourceMapPathOverrides": { 14 | "webpack:///src/*": "${webRoot}/*" 15 | } 16 | }, { 17 | "name": "tests", 18 | "type": "node", 19 | "request": "launch", 20 | "program": "${workspaceRoot}/node_modules/jest-cli/bin/jest.js", 21 | "stopOnEntry": false, 22 | "args": ["--runInBand", "--env=jsdom"], 23 | "cwd": "${workspaceRoot}", 24 | "preLaunchTask": null, 25 | "runtimeExecutable": null, 26 | "runtimeArgs": [ 27 | "--nolazy" 28 | ], 29 | "env": { 30 | "NODE_ENV": "development" 31 | }, 32 | // "externalConsole": false, 33 | "sourceMaps": false 34 | // "outDir": null 35 | }] 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["codecov", "heroku"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2017, devrsion 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-2048-game | [中文](/README_CN.md) 2 | 3 | [![travis-ci](https://travis-ci.org/devrsi0n/React-2048-game.svg?branch=master)](https://travis-ci.org/devrsi0n/React-2048-game) 4 | [![codecov](https://codecov.io/gh/devrsi0n/React-2048-game/branch/master/graph/badge.svg)](https://codecov.io/gh/devrsi0n/React-2048-game) 5 | [![node](https://img.shields.io/badge/node-%20%3E%3D%206.10-brightgreen.svg)](https://nodejs.org) 6 | [![tested with jest](https://img.shields.io/badge/tested_with-jest-99424f.svg)](https://github.com/facebook/jest) 7 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 8 | 9 |   10 | 11 | [![React](/internals/img/react-padded-90.png)](https://facebook.github.io/react/) 12 | [![Webpack](/internals/img/webpack-padded-90.png)](https://webpack.github.io/) 13 | [![Redux](/internals/img/redux-padded-90.png)](http://redux.js.org/) 14 | [![React Router](/internals/img/react-router-padded-90.png)](https://github.com/ReactTraining/react-router) 15 | [![Redux saga](/internals/img/redux-saga-padded-90.png)](https://github.com/ReactTraining/react-router) 16 | [![ESLint](/internals/img/eslint-padded-90.png)](http://eslint.org/) 17 | [![Jest](/internals/img/jest-padded-90.png)](https://facebook.github.io/jest/) 18 | [![Yarn](/internals/img/yarn-padded-90.png)](https://yarnpkg.com/) 19 | 20 |   21 | 22 | A fancy 2048 game build with [react](https://github.com/facebook/react), [redux](https://github.com/reactjs/redux) best practices. Uses many awesome open source tools to improve code styles, includes [eslint](https://github.com/eslint/eslint), [stylelint](https://github.com/stylelint/stylelint), [prettier](https://github.com/prettier/prettier), and [Travis](https://travis-ci.org), [codecov](https://codecov.io) continuous integration services to be guaranteed for code quality and deploy application automatically. Show ❤️ by 🌟. 23 | 24 | 👉 [Start the game](https://re2048.herokuapp.com/) 25 | 26 | ## Quick start 27 | 28 | Require nodejs version > 6.10 as config scripts were wrote in es6+, also recommend to use [yarn](https://yarnpkg.com) to manage project dependencies instead of npm. After fork the project following below commands to get started. 29 | 30 | ```bash 31 | npm i -g yarn # Install yarn 32 | git clone git@github.com:/React-2048-game.git 33 | cd React-2048-game 34 | yarn # Install dependencies 35 | yarn start # Start local developing mode 36 | yarn test # Run test code 37 | yarn build # Build the project from source 38 | ``` 39 | 40 | ## Technologies 41 | 42 | This app has a rich development experience using the following technologies: 43 | 44 | | **Tech** | **Description** |**Learn More**| 45 | |----------|-------|---| 46 | | [React](https://facebook.github.io/react/) | Declarative, component based view library. | [Codecademy Course](https://www.codecademy.com/learn/react-101) | 47 | | [Redux](http://redux.js.org) | A predictable state container. Supports time-travel debugging. | [Learn Redux](https://egghead.io/courses/getting-started-with-redux)| 48 | | [Redux saga](https://redux-saga.js.org/) | Handles side effect model for Redux apps. | [Async operations using redux-saga](https://medium.freecodecamp.org/async-operations-using-redux-saga-2ba02ae077b3)| 49 | | [React Router](https://github.com/reactjs/react-router) | Declarative, multi-platform routing library for React | [Official guide](https://reacttraining.com/react-router/web/guides/philosophy) | 50 | | [Babel](http://babeljs.io) | Compiles ES6 to ES5. Uses the latest features of JavaScript today. | [Learn ES2015](https://babeljs.io/learn-es2015/) | 51 | | [Webpack](https://webpack.js.org) | Bundles npm packages and all kinds of assets, scripts into a single app. Supports hot reloading via [webpack-dev-server](https://github.com/webpack/webpack-dev-server). | [Learn Webpack in 15 Minutes](https://tutorialzine.com/2017/04/learn-webpack-in-15-minutes)| 52 | | [Jest](https://facebook.github.io/jest/) | Tests JavaScript with built-in assertions automatically, integrates with [Enzyme](https://github.com/airbnb/enzyme) for DOM testing in node environment. | [Official guide](https://facebook.github.io/jest/docs/en/getting-started.html) | 53 | | [ESLint](http://eslint.org/)| Analyze and lint JavaScripts. Reports syntax and style issues. | [ESLint rules](https://eslint.org/docs/rules/) | 54 | [SCSS](http://sass-lang.com/) | Powerful professional grade CSS extension language with variables, mixin, functions, and more. | [Getting Sassy with CSS](http://www.sassshop.com/#/1/2)| 55 | 56 | ## License 57 | 58 | [MIT](http://opensource.org/licenses/MIT) 59 | 60 | ## More products from author 61 | 62 | [![Quotion - Apple Notes to blogs in minutes](./.github/quotion.png)](https://quotion.co) 63 | 64 | [Chirpy - Open source & privacy-friendly Disqus alternate for your community](https://chirpy.dev/) 65 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. 31 | // https://github.com/motdotla/dotenv 32 | dotenvFiles.forEach(dotenvFile => { 33 | if (fs.existsSync(dotenvFile)) { 34 | require('dotenv').config({ 35 | path: dotenvFile, 36 | }); 37 | } 38 | }); 39 | 40 | // We support resolving modules according to `NODE_PATH`. 41 | // This lets you use absolute paths in imports inside large monorepos: 42 | // https://github.com/facebookincubator/create-react-app/issues/253. 43 | // It works similar to `NODE_PATH` in Node itself: 44 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 45 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 46 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 47 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 48 | // We also resolve them to make sure all tools using them work consistently. 49 | const appDirectory = fs.realpathSync(process.cwd()); 50 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 51 | .split(path.delimiter) 52 | .filter(folder => folder && !path.isAbsolute(folder)) 53 | .map(folder => path.resolve(appDirectory, folder)) 54 | .join(path.delimiter); 55 | 56 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 57 | // injected into the application via DefinePlugin in Webpack configuration. 58 | const REACT_APP = /^REACT_APP_/i; 59 | 60 | function getClientEnvironment(publicUrl) { 61 | const raw = Object.keys(process.env) 62 | .filter(key => REACT_APP.test(key)) 63 | .reduce( 64 | (env, key) => { 65 | env[key] = process.env[key]; 66 | return env; 67 | }, 68 | { 69 | // Useful for determining whether we’re running in production mode. 70 | // Most importantly, it switches React into the correct mode. 71 | NODE_ENV: process.env.NODE_ENV || 'development', 72 | // Useful for resolving the correct path to static assets in `public`. 73 | // For example, . 74 | // This should only be used as an escape hatch. Normally you would put 75 | // images into the `src` and `import` them in code to get their paths. 76 | PUBLIC_URL: publicUrl, 77 | } 78 | ); 79 | // Stringify all values so we can feed into Webpack DefinePlugin 80 | const stringified = { 81 | 'process.env': Object.keys(raw).reduce((env, key) => { 82 | env[key] = JSON.stringify(raw[key]); 83 | return env; 84 | }, {}), 85 | }; 86 | 87 | return { raw, stringified }; 88 | } 89 | 90 | module.exports = getClientEnvironment; 91 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right 7 | 13 | 14 | <% } %> 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 37 | React 2048 game 38 | 39 | 40 | 43 |
44 | 54 | <% if (process.env.NODE_ENV === 'production') { %> 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | <% } %> 63 | 64 | 65 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React 2048", 3 | "name": "2048 game", 4 | "icons": [ 5 | { 6 | "src": "favicon.png", 7 | "sizes": "450x450", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "favicon-maskable.png", 12 | "sizes": "550x550", 13 | "type": "image/png", 14 | "purpose": "maskable" 15 | } 16 | ], 17 | "start_url": "./index.html", 18 | "display": "standalone", 19 | "theme_color": "#000000", 20 | "background_color": "#ffffff" 21 | } 22 | -------------------------------------------------------------------------------- /public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | https://re2048.herokuapp.com/ 11 | 2017-10-31T02:34:45+00:00 12 | daily 13 | 14 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'production'; 5 | process.env.NODE_ENV = 'production'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const path = require('path'); 18 | const chalk = require('chalk'); 19 | const fs = require('fs-extra'); 20 | const webpack = require('webpack'); 21 | const config = require('../config/webpack.config.prod'); 22 | const paths = require('../config/paths'); 23 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 24 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 25 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 26 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 27 | const printBuildError = require('react-dev-utils/printBuildError'); 28 | 29 | const measureFileSizesBeforeBuild = 30 | FileSizeReporter.measureFileSizesBeforeBuild; 31 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 32 | const useYarn = fs.existsSync(paths.yarnLockFile); 33 | 34 | // These sizes are pretty large. We'll warn for bundles exceeding them. 35 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 36 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 37 | 38 | // Warn and crash if required files are missing 39 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 40 | process.exit(1); 41 | } 42 | 43 | // First, read the current file sizes in build directory. 44 | // This lets us display how much they changed later. 45 | measureFileSizesBeforeBuild(paths.appBuild) 46 | .then(previousFileSizes => { 47 | // Remove all content but keep the directory so that 48 | // if you're in it, you don't end up in Trash 49 | fs.emptyDirSync(paths.appBuild); 50 | // Merge with the public folder 51 | copyPublicFolder(); 52 | // Start the webpack build 53 | return build(previousFileSizes); 54 | }) 55 | .then( 56 | ({ stats, previousFileSizes, warnings }) => { 57 | if (warnings.length) { 58 | console.log(chalk.yellow('Compiled with warnings.\n')); 59 | console.log(warnings.join('\n\n')); 60 | console.log( 61 | '\nSearch for the ' + 62 | chalk.underline(chalk.yellow('keywords')) + 63 | ' to learn more about each warning.' 64 | ); 65 | console.log( 66 | 'To ignore, add ' + 67 | chalk.cyan('// eslint-disable-next-line') + 68 | ' to the line before.\n' 69 | ); 70 | } else { 71 | console.log(chalk.green('Compiled successfully.\n')); 72 | } 73 | 74 | console.log('File sizes after gzip:\n'); 75 | printFileSizesAfterBuild( 76 | stats, 77 | previousFileSizes, 78 | paths.appBuild, 79 | WARN_AFTER_BUNDLE_GZIP_SIZE, 80 | WARN_AFTER_CHUNK_GZIP_SIZE 81 | ); 82 | console.log(); 83 | 84 | const appPackage = require(paths.appPackageJson); 85 | const publicUrl = paths.publicUrl; 86 | const publicPath = config.output.publicPath; 87 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 88 | printHostingInstructions( 89 | appPackage, 90 | publicUrl, 91 | publicPath, 92 | buildFolder, 93 | useYarn 94 | ); 95 | }, 96 | err => { 97 | console.log(chalk.red('Failed to compile.\n')); 98 | printBuildError(err); 99 | process.exit(1); 100 | } 101 | ); 102 | 103 | // Create the production build and print the deployment instructions. 104 | function build(previousFileSizes) { 105 | console.log('Creating an optimized production build...'); 106 | 107 | let compiler = webpack(config); 108 | return new Promise((resolve, reject) => { 109 | compiler.run((err, stats) => { 110 | if (err) { 111 | return reject(err); 112 | } 113 | const messages = formatWebpackMessages(stats.toJson({}, true)); 114 | if (messages.errors.length) { 115 | // Only keep the first error. Others are often indicative 116 | // of the same problem, but confuse the reader with noise. 117 | if (messages.errors.length > 1) { 118 | messages.errors.length = 1; 119 | } 120 | return reject(new Error(messages.errors.join('\n\n'))); 121 | } 122 | if ( 123 | process.env.CI && 124 | (typeof process.env.CI !== 'string' || 125 | process.env.CI.toLowerCase() !== 'false') && 126 | messages.warnings.length 127 | ) { 128 | console.log( 129 | chalk.yellow( 130 | '\nTreating warnings as errors because process.env.CI = true.\n' + 131 | 'Most CI servers set it automatically.\n' 132 | ) 133 | ); 134 | return reject(new Error(messages.warnings.join('\n\n'))); 135 | } 136 | return resolve({ 137 | stats, 138 | previousFileSizes, 139 | warnings: messages.warnings, 140 | }); 141 | }); 142 | }); 143 | } 144 | 145 | function copyPublicFolder() { 146 | fs.copySync(paths.appPublic, paths.appBuild, { 147 | dereference: true, 148 | filter: file => file !== paths.appHtml, 149 | }); 150 | } 151 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo $PWD 4 | npm run build 5 | cd ../re2048 6 | git add . 7 | git commit -m "update frontend" 8 | git push heroku master 9 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const fs = require('fs'); 18 | const chalk = require('chalk'); 19 | const webpack = require('webpack'); 20 | const WebpackDevServer = require('webpack-dev-server'); 21 | const clearConsole = require('react-dev-utils/clearConsole'); 22 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 23 | const { 24 | choosePort, 25 | createCompiler, 26 | prepareProxy, 27 | prepareUrls, 28 | } = require('react-dev-utils/WebpackDevServerUtils'); 29 | const openBrowser = require('react-dev-utils/openBrowser'); 30 | const paths = require('../config/paths'); 31 | const config = require('../config/webpack.config.dev'); 32 | const createDevServerConfig = require('../config/webpackDevServer.config'); 33 | 34 | const useYarn = fs.existsSync(paths.yarnLockFile); 35 | const isInteractive = process.stdout.isTTY; 36 | 37 | // Warn and crash if required files are missing 38 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 39 | process.exit(1); 40 | } 41 | 42 | // Tools like Cloud9 rely on this. 43 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 44 | const HOST = process.env.HOST || '0.0.0.0'; 45 | 46 | // We attempt to use the default port but if it is busy, we offer the user to 47 | // run on a different port. `detect()` Promise resolves to the next free port. 48 | choosePort(HOST, DEFAULT_PORT) 49 | .then(port => { 50 | if (port == null) { 51 | // We have not found a port. 52 | return; 53 | } 54 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 55 | const appName = require(paths.appPackageJson).name; 56 | const urls = prepareUrls(protocol, HOST, port); 57 | // Create a webpack compiler that is configured with custom messages. 58 | const compiler = createCompiler(webpack, config, appName, urls, useYarn); 59 | // Load proxy config 60 | const proxySetting = require(paths.appPackageJson).proxy; 61 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 62 | // Serve webpack assets generated by the compiler over a web sever. 63 | const serverConfig = createDevServerConfig( 64 | proxyConfig, 65 | urls.lanUrlForConfig 66 | ); 67 | const devServer = new WebpackDevServer(compiler, serverConfig); 68 | // Launch WebpackDevServer. 69 | devServer.listen(port, HOST, err => { 70 | if (err) { 71 | return console.log(err); 72 | } 73 | if (isInteractive) { 74 | clearConsole(); 75 | } 76 | console.log(chalk.cyan('Starting the development server...\n')); 77 | openBrowser(urls.localUrlForBrowser); 78 | }); 79 | 80 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 81 | process.on(sig, function() { 82 | devServer.close(); 83 | process.exit(); 84 | }); 85 | }); 86 | }) 87 | .catch(err => { 88 | if (err && err.message) { 89 | console.log(err.message); 90 | } 91 | process.exit(1); 92 | }); 93 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = "test"; 5 | process.env.NODE_ENV = "test"; 6 | process.env.PUBLIC_URL = ""; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on("unhandledRejection", err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require("../config/env"); 17 | 18 | const jest = require("jest"); 19 | const argv = process.argv.slice(2); 20 | 21 | // Watch unless on PRE_PUSH check, CI or in coverage mode 22 | if ( 23 | !process.env.PRE_PUSH && 24 | !process.env.CI && 25 | argv.indexOf("--coverage") < 0 26 | ) { 27 | argv.push("--watch"); 28 | } 29 | 30 | jest.run(argv); 31 | -------------------------------------------------------------------------------- /src/apis/index.js: -------------------------------------------------------------------------------- 1 | function* githubGet(url, token) { 2 | return yield fetch(`https://api.github.com${url}`, { 3 | method: 'GET', 4 | headers: { 5 | Authorization: `token ${token}`, 6 | Accept: 'application/vnd.github.v3+json' 7 | }, 8 | credentials: 'same-origin' 9 | }); 10 | } 11 | 12 | const host = window.location.origin; 13 | 14 | function* serverGet(url) { 15 | return yield fetch(`${host}${url}`, { 16 | method: 'GET', 17 | credentials: 'same-origin' 18 | }); 19 | } 20 | 21 | function* serverPut(url, data) { 22 | return yield fetch(`${host}${url}`, { 23 | method: 'PUT', 24 | body: JSON.stringify(data), 25 | headers: { 26 | 'Content-Type': 'application/json' 27 | }, 28 | credentials: 'same-origin' 29 | }); 30 | } 31 | 32 | const url = '/rank'; 33 | 34 | // Ranking list data saved in egg server. 35 | export function* getRankingList() { 36 | let rsp = yield serverGet(url); 37 | rsp = yield rsp.json(); 38 | const { list } = rsp; 39 | return list.sort((a, b) => b.score - a.score); 40 | } 41 | 42 | export function* getUserInfo() { 43 | const info = localStorage.getItem('USER_INFO'); 44 | if (info && info.name) { 45 | return info; 46 | } 47 | 48 | // Reuse gitalk access token 49 | const token = localStorage.getItem('GT_ACCESS_TOKEN'); 50 | if (!token) { 51 | console.log('Must login to upload score'); 52 | return null; 53 | } 54 | let rsp = yield githubGet('/user', token); 55 | rsp = yield rsp.json(); 56 | console.log(rsp); 57 | if (rsp && rsp.name) { 58 | localStorage.setItem('USER_INFO', JSON.stringify(rsp)); 59 | } 60 | return rsp; 61 | } 62 | 63 | export function* updateRankingList(list) { 64 | console.log('list', list); 65 | let rsp = yield serverPut(url, { list }); 66 | rsp = yield rsp.json(); 67 | return rsp.list.sort((a, b) => b.score - a.score); 68 | } 69 | -------------------------------------------------------------------------------- /src/assets/audio/move.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devrsi0n/React-2048-game/d348adece13e466a35bdfd37acf45486d39beec0/src/assets/audio/move.mp3 -------------------------------------------------------------------------------- /src/assets/audio/popup.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devrsi0n/React-2048-game/d348adece13e466a35bdfd37acf45486d39beec0/src/assets/audio/popup.mp3 -------------------------------------------------------------------------------- /src/assets/images/github-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devrsi0n/React-2048-game/d348adece13e466a35bdfd37acf45486d39beec0/src/assets/images/github-white.png -------------------------------------------------------------------------------- /src/assets/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devrsi0n/React-2048-game/d348adece13e466a35bdfd37acf45486d39beec0/src/assets/images/github.png -------------------------------------------------------------------------------- /src/assets/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './vars'; 2 | 3 | // global styles 4 | 5 | :global { 6 | html { 7 | font-size: 1vw; 8 | touch-action: manipulation; 9 | } 10 | 11 | body { 12 | background: #fff; 13 | font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | @media all and (max-width: $break-point) { 19 | html { 20 | font-size: 4vw; 21 | } 22 | } 23 | 24 | input, 25 | button, 26 | submit { 27 | border: 0; 28 | } 29 | 30 | h1, 31 | h2, 32 | h3, 33 | h4, 34 | h5, 35 | h6, 36 | p { 37 | margin: 0; 38 | } 39 | 40 | img { 41 | max-width: 100%; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/assets/styles/normalize.scss: -------------------------------------------------------------------------------- 1 | // stylelint-disable 2 | 3 | :global { 4 | /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in 9 | * IE on Windows Phone and in iOS. 10 | */ 11 | 12 | html { 13 | line-height: 1.15; /* 1 */ 14 | -ms-text-size-adjust: 100%; /* 2 */ 15 | -webkit-text-size-adjust: 100%; /* 2 */ 16 | } 17 | 18 | /* Sections 19 | ========================================================================== */ 20 | 21 | /** 22 | * Remove the margin in all browsers (opinionated). 23 | */ 24 | 25 | body { 26 | margin: 0; 27 | } 28 | 29 | /** 30 | * Add the correct display in IE 9-. 31 | */ 32 | 33 | article, 34 | aside, 35 | footer, 36 | header, 37 | nav, 38 | section { 39 | display: block; 40 | } 41 | 42 | /** 43 | * Correct the font size and margin on `h1` elements within `section` and 44 | * `article` contexts in Chrome, Firefox, and Safari. 45 | */ 46 | 47 | h1 { 48 | font-size: 2em; 49 | margin: 0.67em 0; 50 | } 51 | 52 | /* Grouping content 53 | ========================================================================== */ 54 | 55 | /** 56 | * Add the correct display in IE 9-. 57 | * 1. Add the correct display in IE. 58 | */ 59 | 60 | figcaption, 61 | figure, 62 | main { /* 1 */ 63 | display: block; 64 | } 65 | 66 | /** 67 | * Add the correct margin in IE 8. 68 | */ 69 | 70 | figure { 71 | margin: 1em 40px; 72 | } 73 | 74 | /** 75 | * 1. Add the correct box sizing in Firefox. 76 | * 2. Show the overflow in Edge and IE. 77 | */ 78 | 79 | hr { 80 | box-sizing: content-box; /* 1 */ 81 | height: 0; /* 1 */ 82 | overflow: visible; /* 2 */ 83 | } 84 | 85 | /** 86 | * 1. Correct the inheritance and scaling of font size in all browsers. 87 | * 2. Correct the odd `em` font sizing in all browsers. 88 | */ 89 | 90 | pre { 91 | font-family: monospace; /* 1 */ 92 | font-size: 1em; /* 2 */ 93 | } 94 | 95 | /* Text-level semantics 96 | ========================================================================== */ 97 | 98 | /** 99 | * 1. Remove the gray background on active links in IE 10. 100 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 101 | */ 102 | 103 | a { 104 | background-color: transparent; /* 1 */ 105 | -webkit-text-decoration-skip: objects; /* 2 */ 106 | } 107 | 108 | /** 109 | * 1. Remove the bottom border in Chrome 57- and Firefox 39-. 110 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 111 | */ 112 | 113 | abbr[title] { 114 | border-bottom: none; /* 1 */ 115 | text-decoration: underline; /* 2 */ 116 | text-decoration: underline dotted; /* 2 */ 117 | } 118 | 119 | /** 120 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: inherit; 126 | } 127 | 128 | /** 129 | * Add the correct font weight in Chrome, Edge, and Safari. 130 | */ 131 | 132 | b, 133 | strong { 134 | font-weight: bolder; 135 | } 136 | 137 | /** 138 | * 1. Correct the inheritance and scaling of font size in all browsers. 139 | * 2. Correct the odd `em` font sizing in all browsers. 140 | */ 141 | 142 | code, 143 | kbd, 144 | samp { 145 | font-family: monospace; /* 1 */ 146 | font-size: 1em; /* 2 */ 147 | } 148 | 149 | /** 150 | * Add the correct font style in Android 4.3-. 151 | */ 152 | 153 | dfn { 154 | font-style: italic; 155 | } 156 | 157 | /** 158 | * Add the correct background and color in IE 9-. 159 | */ 160 | 161 | mark { 162 | background-color: #ff0; 163 | color: #000; 164 | } 165 | 166 | /** 167 | * Add the correct font size in all browsers. 168 | */ 169 | 170 | small { 171 | font-size: 80%; 172 | } 173 | 174 | /** 175 | * Prevent `sub` and `sup` elements from affecting the line height in 176 | * all browsers. 177 | */ 178 | 179 | sub, 180 | sup { 181 | font-size: 75%; 182 | line-height: 0; 183 | position: relative; 184 | vertical-align: baseline; 185 | } 186 | 187 | sub { 188 | bottom: -0.25em; 189 | } 190 | 191 | sup { 192 | top: -0.5em; 193 | } 194 | 195 | /* Embedded content 196 | ========================================================================== */ 197 | 198 | /** 199 | * Add the correct display in IE 9-. 200 | */ 201 | 202 | audio, 203 | video { 204 | display: inline-block; 205 | } 206 | 207 | /** 208 | * Add the correct display in iOS 4-7. 209 | */ 210 | 211 | audio:not([controls]) { 212 | display: none; 213 | height: 0; 214 | } 215 | 216 | /** 217 | * Remove the border on images inside links in IE 10-. 218 | */ 219 | 220 | img { 221 | border-style: none; 222 | } 223 | 224 | /** 225 | * Hide the overflow in IE. 226 | */ 227 | 228 | svg:not(:root) { 229 | overflow: hidden; 230 | } 231 | 232 | /* Forms 233 | ========================================================================== */ 234 | 235 | /** 236 | * 1. Change the font styles in all browsers (opinionated). 237 | * 2. Remove the margin in Firefox and Safari. 238 | */ 239 | 240 | button, 241 | input, 242 | optgroup, 243 | select, 244 | textarea { 245 | font-family: sans-serif; /* 1 */ 246 | font-size: 100%; /* 1 */ 247 | line-height: 1.15; /* 1 */ 248 | margin: 0; /* 2 */ 249 | } 250 | 251 | /** 252 | * Show the overflow in IE. 253 | * 1. Show the overflow in Edge. 254 | */ 255 | 256 | button, 257 | input { /* 1 */ 258 | overflow: visible; 259 | } 260 | 261 | /** 262 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 263 | * 1. Remove the inheritance of text transform in Firefox. 264 | */ 265 | 266 | button, 267 | select { /* 1 */ 268 | text-transform: none; 269 | } 270 | 271 | /** 272 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 273 | * controls in Android 4. 274 | * 2. Correct the inability to style clickable types in iOS and Safari. 275 | */ 276 | 277 | button, 278 | html [type="button"], /* 1 */ 279 | [type="reset"], 280 | [type="submit"] { 281 | -webkit-appearance: button; /* 2 */ 282 | } 283 | 284 | /** 285 | * Remove the inner border and padding in Firefox. 286 | */ 287 | 288 | button::-moz-focus-inner, 289 | [type="button"]::-moz-focus-inner, 290 | [type="reset"]::-moz-focus-inner, 291 | [type="submit"]::-moz-focus-inner { 292 | border-style: none; 293 | padding: 0; 294 | } 295 | 296 | /** 297 | * Restore the focus styles unset by the previous rule. 298 | */ 299 | 300 | button:-moz-focusring, 301 | [type="button"]:-moz-focusring, 302 | [type="reset"]:-moz-focusring, 303 | [type="submit"]:-moz-focusring { 304 | outline: 1px dotted ButtonText; 305 | } 306 | 307 | /** 308 | * Correct the padding in Firefox. 309 | */ 310 | 311 | fieldset { 312 | padding: 0.35em 0.75em 0.625em; 313 | } 314 | 315 | /** 316 | * 1. Correct the text wrapping in Edge and IE. 317 | * 2. Correct the color inheritance from `fieldset` elements in IE. 318 | * 3. Remove the padding so developers are not caught out when they zero out 319 | * `fieldset` elements in all browsers. 320 | */ 321 | 322 | legend { 323 | box-sizing: border-box; /* 1 */ 324 | color: inherit; /* 2 */ 325 | display: table; /* 1 */ 326 | max-width: 100%; /* 1 */ 327 | padding: 0; /* 3 */ 328 | white-space: normal; /* 1 */ 329 | } 330 | 331 | /** 332 | * 1. Add the correct display in IE 9-. 333 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. 334 | */ 335 | 336 | progress { 337 | display: inline-block; /* 1 */ 338 | vertical-align: baseline; /* 2 */ 339 | } 340 | 341 | /** 342 | * Remove the default vertical scrollbar in IE. 343 | */ 344 | 345 | textarea { 346 | overflow: auto; 347 | } 348 | 349 | /** 350 | * 1. Add the correct box sizing in IE 10-. 351 | * 2. Remove the padding in IE 10-. 352 | */ 353 | 354 | [type="checkbox"], 355 | [type="radio"] { 356 | box-sizing: border-box; /* 1 */ 357 | padding: 0; /* 2 */ 358 | } 359 | 360 | /** 361 | * Correct the cursor style of increment and decrement buttons in Chrome. 362 | */ 363 | 364 | [type="number"]::-webkit-inner-spin-button, 365 | [type="number"]::-webkit-outer-spin-button { 366 | height: auto; 367 | } 368 | 369 | /** 370 | * 1. Correct the odd appearance in Chrome and Safari. 371 | * 2. Correct the outline style in Safari. 372 | */ 373 | 374 | [type="search"] { 375 | -webkit-appearance: textfield; /* 1 */ 376 | outline-offset: -2px; /* 2 */ 377 | } 378 | 379 | /** 380 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 381 | */ 382 | 383 | [type="search"]::-webkit-search-cancel-button, 384 | [type="search"]::-webkit-search-decoration { 385 | -webkit-appearance: none; 386 | } 387 | 388 | /** 389 | * 1. Correct the inability to style clickable types in iOS and Safari. 390 | * 2. Change font properties to `inherit` in Safari. 391 | */ 392 | 393 | ::-webkit-file-upload-button { 394 | -webkit-appearance: button; /* 1 */ 395 | font: inherit; /* 2 */ 396 | } 397 | 398 | /* Interactive 399 | ========================================================================== */ 400 | 401 | /* 402 | * Add the correct display in IE 9-. 403 | * 1. Add the correct display in Edge, IE, and Firefox. 404 | */ 405 | 406 | details, /* 1 */ 407 | menu { 408 | display: block; 409 | } 410 | 411 | /* 412 | * Add the correct display in all browsers. 413 | */ 414 | 415 | summary { 416 | display: list-item; 417 | } 418 | 419 | /* Scripting 420 | ========================================================================== */ 421 | 422 | /** 423 | * Add the correct display in IE 9-. 424 | */ 425 | 426 | canvas { 427 | display: inline-block; 428 | } 429 | 430 | /** 431 | * Add the correct display in IE. 432 | */ 433 | 434 | template { 435 | display: none; 436 | } 437 | 438 | /* Hidden 439 | ========================================================================== */ 440 | 441 | /** 442 | * Add the correct display in IE 10-. 443 | */ 444 | 445 | [hidden] { 446 | display: none; 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /src/assets/styles/vars.scss: -------------------------------------------------------------------------------- 1 | // global scss variables 2 | 3 | $red: #ff5252; 4 | $pink: #e91e63; 5 | $purple: #9c27b0; 6 | $deep-purple: #673ab7; 7 | $indigo: #3f51b5; 8 | $blue: #2196f3; 9 | $light-blue: #03a9f4; 10 | $cyan: #00bcd4; 11 | $teal: #009688; 12 | $green: #4caf50; 13 | $light-green: #8bc34a; 14 | $lime: #cddc39; 15 | $yellow: #ffeb3b; 16 | $amber: #ffc107; 17 | $orange: #ff9800; 18 | $deep-orange: #ff5722; 19 | $brown: #795548; 20 | $grey: #9e9e9e; 21 | 22 | $white: #fff; 23 | $gray: #f7f7f7; 24 | $black: #303030; 25 | $light-black: #666; 26 | $peach: #ff4081; 27 | 28 | $btn-bg: $black; 29 | $btn-color: $white; 30 | 31 | $border-color: #e1e4e8; 32 | 33 | // sizes 34 | $break-point: 768px; 35 | $mobile: 'all and (max-width : #{$break-point})'; 36 | 37 | 38 | @function get-vw($object) { 39 | $vw: (1440 * 0.01) * 1px; 40 | @return ($object / $vw) * 1vw; 41 | } 42 | -------------------------------------------------------------------------------- /src/assets/svg/arrow.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/svg/reset.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/svg/speaker-off.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/svg/speaker-on.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/svg/undo.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/Board/__tests__/Board.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Enzyme, { mount } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import Board from '..'; 6 | 7 | Enzyme.configure({ adapter: new Adapter() }); 8 | 9 | const MATRIX = [[2, 0, 0, 0], [0, 0, 0, 0], [0, 0, 4, 0], [0, 0, 0, 0]]; 10 | 11 | describe('', () => { 12 | it('component render', () => { 13 | const board = renderer.create().toJSON(); 14 | expect(board).toMatchSnapshot(); 15 | }); 16 | 17 | it('shouldComponentUpdate', () => { 18 | const speaker = mount(); 19 | expect(speaker.props().matrix).toEqual(MATRIX); 20 | let shouldUpdate = speaker.instance().shouldComponentUpdate( 21 | { 22 | matrix: MATRIX 23 | }, 24 | null 25 | ); 26 | expect(shouldUpdate).toBe(false); 27 | shouldUpdate = speaker.instance().shouldComponentUpdate( 28 | { 29 | matrix: [[2, 2, 2, 2], [0, 0, 0, 0], [0, 0, 4, 0], [0, 0, 0, 0]] 30 | }, 31 | null 32 | ); 33 | expect(shouldUpdate).toBe(true); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/components/Board/__tests__/__snapshots__/Board.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` component render 1`] = ` 4 | 7 | 8 | 9 | 20 | 29 | 38 | 47 | 48 | 49 | 58 | 67 | 76 | 85 | 86 | 87 | 96 | 105 | 116 | 125 | 126 | 127 | 136 | 145 | 154 | 163 | 164 | 165 |
10 |
13 |
16 | 2 17 |
18 |
19 |
21 |
24 |
27 |
28 |
30 |
33 |
36 |
37 |
39 |
42 |
45 |
46 |
50 |
53 |
56 |
57 |
59 |
62 |
65 |
66 |
68 |
71 |
74 |
75 |
77 |
80 |
83 |
84 |
88 |
91 |
94 |
95 |
97 |
100 |
103 |
104 |
106 |
109 |
112 | 4 113 |
114 |
115 |
117 |
120 |
123 |
124 |
128 |
131 |
134 |
135 |
137 |
140 |
143 |
144 |
146 |
149 |
152 |
153 |
155 |
158 |
161 |
162 |
166 | `; 167 | -------------------------------------------------------------------------------- /src/components/Board/board.scss: -------------------------------------------------------------------------------- 1 | @import '../../assets/styles/vars'; 2 | 3 | .board { 4 | border-collapse: separate; 5 | border-spacing: 0.25vw; 6 | } 7 | 8 | @media all and (max-width: $break-point) { 9 | .board { 10 | border-spacing: 1vw; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Board/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Row from '../Row'; 4 | import styles from './board.scss'; 5 | import { isObjEqual } from '../../utils/helpers'; 6 | 7 | // Game board 8 | export default class Board extends React.Component { 9 | static propTypes = { 10 | matrix: PropTypes.arrayOf(PropTypes.array).isRequired 11 | }; 12 | 13 | shouldComponentUpdate(nextProps, nextState) { 14 | return ( 15 | !isObjEqual(nextProps, this.props) || !isObjEqual(nextState, this.state) 16 | ); 17 | } 18 | 19 | render() { 20 | const { props: { matrix } } = this; 21 | 22 | return ( 23 | 24 | 25 | {/* eslint-disable react/no-array-index-key */} 26 | {matrix.map((row, idx) => )} 27 | 28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/Button/__tests__/Button.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Button from '..'; 4 | 5 | describe('', () => { 6 | it('component render', () => { 7 | let board = renderer.create( 18 | ) 19 | .toJSON(); 20 | expect(board).toMatchSnapshot(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/components/Button/__tests__/__snapshots__/Button.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` component render 1`] = ` 4 | 10 | `; 11 | 12 | exports[` component render 2`] = ` 13 | 19 | `; 20 | 21 | exports[` component render 3`] = ` 22 | 30 | `; 31 | -------------------------------------------------------------------------------- /src/components/Button/button.scss: -------------------------------------------------------------------------------- 1 | @import '../../assets/styles/vars'; 2 | 3 | /* stylelint-disable max-nesting-depth */ 4 | :global { 5 | .btn-default { 6 | color: #fff; 7 | } 8 | 9 | .btn-primary, 10 | .btn, 11 | .btn-lg, 12 | .btn-md, 13 | .btn-sm, 14 | .btn-xs { 15 | color: $blue; 16 | } 17 | 18 | .btn-warn { 19 | color: $yellow; 20 | } 21 | 22 | .btn-danger { 23 | color: $red; 24 | } 25 | 26 | .btn-success { 27 | color: $green; 28 | } 29 | 30 | .btn-royal { 31 | color: $purple; 32 | } 33 | 34 | .btn, 35 | .btn-lg, 36 | .btn-md, 37 | .btn-sm, 38 | .btn-xs { 39 | background: transparent; 40 | border-color: transparent; 41 | border-width: 0; 42 | cursor: pointer; 43 | font-weight: 400; 44 | margin: 0; 45 | padding: 0; 46 | position: relative; 47 | } 48 | 49 | .btn-lg { 50 | font-family: inherit; 51 | font-size: 2.2rem; 52 | padding: 0.62vw 1.15vw; 53 | } 54 | 55 | .btn-md { 56 | font-family: inherit; 57 | font-size: 2rem; 58 | padding: 0.38vw 0.92vw; 59 | } 60 | 61 | .btn-sm { 62 | font-family: inherit; 63 | font-size: 1.8rem; 64 | padding: 0.31vw 0.77vw; 65 | } 66 | 67 | .btn-xs { 68 | font-family: inherit; 69 | font-size: 1.6rem; 70 | padding: 0.23vw 0.62vw; 71 | } 72 | 73 | @keyframes :global(ripple) { 74 | 0% { 75 | opacity: 1; 76 | transform: scale(0, 0); 77 | } 78 | 79 | 20% { 80 | opacity: 1; 81 | transform: scale(25, 25); 82 | } 83 | 84 | 100% { 85 | opacity: 0; 86 | transform: scale(40, 40); 87 | } 88 | } 89 | 90 | .btn-flat { 91 | background: transparent; 92 | background: #fff; 93 | border-color: transparent; 94 | border-radius: 2px; 95 | border-width: 0; 96 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.18), 0 1px 5px 0 rgba(0, 0, 0, 0.15); 97 | color: $blue; 98 | cursor: pointer; 99 | font-family: inherit; 100 | font-size: 2rem; 101 | font-weight: 400; 102 | margin: 0; 103 | overflow: hidden; 104 | padding: 0.38vw 0.92vw; 105 | position: relative; 106 | text-transform: uppercase; 107 | transform: translate3d(0, 0, 0); 108 | transition: all 0.25s cubic-bezier(0.02, 0.01, 0.47, 1); 109 | 110 | &:hover, 111 | &:focus { 112 | box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15); 113 | outline: 0; 114 | transition: box-shadow 0.4s ease-out; 115 | } 116 | 117 | &::after { 118 | background: rgba(255, 255, 255, 0.5); 119 | border-radius: 100%; 120 | content: ''; 121 | height: 5px; 122 | left: 50%; 123 | opacity: 0; 124 | position: absolute; 125 | top: 50%; 126 | transform: scale(1, 1) translate(-50%); 127 | transform-origin: 50% 50%; 128 | width: 5px; 129 | } 130 | 131 | &:focus:not(:active)::after { 132 | animation: ripple 1s ease-out; 133 | } 134 | 135 | &.btn-xs { 136 | font-family: inherit; 137 | font-size: 1.6rem; 138 | padding: 0.23vw 0.62vw; 139 | } 140 | 141 | &.btn-sm { 142 | font-family: inherit; 143 | font-size: 1.8rem; 144 | padding: 0.31vw 0.77vw; 145 | } 146 | 147 | &.btn-md { 148 | font-family: inherit; 149 | font-size: 2rem; 150 | padding: 0.304vw 0.763vw; 151 | } 152 | 153 | // TODO: Add more media query on button size 154 | @media all and (max-width: $break-point) { 155 | &.btn-md { 156 | padding: 1vw 3vw; 157 | } 158 | } 159 | 160 | &.btn-lg { 161 | font-family: inherit; 162 | font-size: 2.2rem; 163 | padding: 0.62vw 1.15vw; 164 | } 165 | 166 | &.btn-default { 167 | background: #fff; 168 | color: $blue; 169 | 170 | &::after { 171 | background: rgba(29, 137, 255, 0.5); 172 | } 173 | } 174 | 175 | &.btn-primary { 176 | background: $blue; 177 | color: #fff; 178 | } 179 | 180 | &.btn-warn { 181 | background: $yellow; 182 | color: #fff; 183 | 184 | &::after { 185 | background: rgba(254, 171, 58, 0.5); 186 | } 187 | } 188 | 189 | &.btn-danger { 190 | background: $red; 191 | color: #fff; 192 | 193 | &::after { 194 | background: rgba(255, 89, 100, 0.5); 195 | } 196 | } 197 | 198 | &.btn-success { 199 | background: $green; 200 | color: #fff; 201 | 202 | &::after { 203 | background: rgba(40, 183, 141, 0.5); 204 | } 205 | } 206 | 207 | &.btn-royal { 208 | background: $purple; 209 | color: #fff; 210 | 211 | &::after { 212 | background: rgba(189, 45, 245, 0.5); 213 | } 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/components/Button/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './button.scss'; 4 | 5 | export default function Button({ children, onClick, type, size }) { 6 | const sizeCls = `btn-${size}`; 7 | const typeCls = `btn-${type}`; 8 | return ( 9 | 12 | ); 13 | } 14 | 15 | Button.propTypes = { 16 | children: PropTypes.oneOfType([PropTypes.node]), 17 | onClick: PropTypes.func, 18 | size: PropTypes.oneOf(['lg', 'md', 'sm', 'xs']), 19 | type: PropTypes.oneOf([ 20 | 'default', 21 | 'primary', 22 | 'warn', 23 | 'danger', 24 | 'success', 25 | 'royal' 26 | ]) 27 | }; 28 | 29 | Button.defaultProps = { 30 | children: '', 31 | onClick() {}, 32 | size: 'md', 33 | type: 'default' 34 | }; 35 | -------------------------------------------------------------------------------- /src/components/Cell/__tests__/Cell.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Enzyme, { mount } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import Cell from '..'; 6 | 7 | Enzyme.configure({ adapter: new Adapter() }); 8 | 9 | describe('', () => { 10 | it('component render', () => { 11 | let cell = renderer.create().toJSON(); 12 | expect(cell).toMatchSnapshot(); 13 | 14 | cell = renderer.create().toJSON(); 15 | expect(cell).toMatchSnapshot(); 16 | }); 17 | 18 | it('shouldComponentUpdate', () => { 19 | const tr = document.createElement('tr'); 20 | const cell = mount(, { 21 | attachTo: tr 22 | }); 23 | expect(cell.props().value).toEqual(32); 24 | let shouldUpdate = cell.instance().shouldComponentUpdate( 25 | { 26 | value: 32 27 | }, 28 | null 29 | ); 30 | expect(shouldUpdate).toBe(false); 31 | shouldUpdate = cell.instance().shouldComponentUpdate( 32 | { 33 | value: 233 34 | }, 35 | null 36 | ); 37 | expect(shouldUpdate).toBe(true); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/components/Cell/__tests__/__snapshots__/Cell.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` component render 1`] = ` 4 | 5 |
8 |
11 | 32 12 |
13 |
14 | 15 | `; 16 | 17 | exports[` component render 2`] = ` 18 | 19 |
22 |
25 | 2048 26 |
27 |
28 | 29 | `; 30 | -------------------------------------------------------------------------------- /src/components/Cell/cell.scss: -------------------------------------------------------------------------------- 1 | @import '../../assets/styles/vars'; 2 | 3 | $web-len: 6vw; 4 | $mobile-len: 20vw; 5 | 6 | .cell { 7 | align-items: center; 8 | background-color: $black; 9 | border-radius: 5px; 10 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.18), 0 1px 5px 0 rgba(0, 0, 0, 0.15); 11 | display: flex; 12 | height: $web-len; 13 | justify-content: center; 14 | width: $web-len; 15 | } 16 | 17 | @media all and (max-width: $break-point) { 18 | .cell { 19 | height: $mobile-len; 20 | width: $mobile-len; 21 | } 22 | } 23 | 24 | .number { 25 | color: $white; 26 | font-size: 2rem; 27 | } 28 | 29 | .color-2 { 30 | background-color: $blue; 31 | } 32 | 33 | .color-4 { 34 | background-color: $green; 35 | } 36 | 37 | .color-8 { 38 | background-color: $purple; 39 | } 40 | 41 | .color-16 { 42 | background-color: $red; 43 | } 44 | 45 | .color-32 { 46 | background-color: $pink; 47 | } 48 | 49 | .color-64 { 50 | background-color: $lime; 51 | } 52 | 53 | .color-128 { 54 | background-color: $indigo; 55 | } 56 | 57 | .color-256 { 58 | background-color: $orange; 59 | } 60 | 61 | .color-512 { 62 | background-color: $deep-purple; 63 | } 64 | 65 | .color-1024 { 66 | background-color: $teal; 67 | } 68 | 69 | .color-2048 { 70 | background-color: $light-blue; 71 | } 72 | 73 | .color-4096 { 74 | background-color: $yellow; 75 | } 76 | 77 | .color-8192 { 78 | background-color: $cyan; 79 | } 80 | -------------------------------------------------------------------------------- /src/components/Cell/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | import styles from './cell.scss'; 5 | import { isObjEqual } from '../../utils/helpers'; 6 | 7 | // Game board cell, minimum component in game board, 8 | // one component stand for a number. 9 | export default class Cell extends React.Component { 10 | static propTypes = { 11 | value: PropTypes.number.isRequired 12 | }; 13 | 14 | shouldComponentUpdate(nextProps, nextState) { 15 | return ( 16 | !isObjEqual(nextProps, this.props) || !isObjEqual(nextState, this.state) 17 | ); 18 | } 19 | 20 | render() { 21 | const { props: { value } } = this; 22 | 23 | const color = `color-${value}`; 24 | return ( 25 | 26 |
29 |
{value || null}
30 |
31 | 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/Comments/__tests__/Comments.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Comments from '..'; 4 | 5 | describe('', () => { 6 | it('component render', () => { 7 | const comments = renderer.create().toJSON(); 8 | expect(comments).toMatchSnapshot(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/Comments/__tests__/__snapshots__/Comments.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` component render 1`] = ` 4 |
7 |
10 |

18 |

22 |
23 |
24 | `; 25 | -------------------------------------------------------------------------------- /src/components/Comments/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import gitalk from '../../utils/gitalk'; 3 | import styles from './comments.scss'; 4 | import i18n from '../../utils/i18n'; 5 | 6 | export default class Comments extends Component { 7 | componentDidMount() { 8 | if (process.env.NODE_ENV !== 'test') { 9 | gitalk.render('gitalk-container'); 10 | } 11 | } 12 | 13 | render() { 14 | return ( 15 |
16 |
17 | {/* eslint-disable react/no-danger */} 18 |

22 |

23 |
24 |
25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/Firework/__tests__/Firework.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Enzyme, { shallow } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import Firework from '..'; 6 | 7 | Enzyme.configure({ adapter: new Adapter() }); 8 | 9 | describe('', () => { 10 | it('component render', () => { 11 | const firework = renderer.create().toJSON(); 12 | expect(firework).toMatchSnapshot(); 13 | }); 14 | 15 | it('shouldComponentUpdate', () => { 16 | const firework = shallow().instance(); 17 | expect(firework.shouldComponentUpdate()).toBe(false); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/components/Firework/__tests__/__snapshots__/Firework.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` component render 1`] = ` 4 |
7 |
10 |
13 |
14 | `; 15 | -------------------------------------------------------------------------------- /src/components/Firework/firework.scss: -------------------------------------------------------------------------------- 1 | @import '../../assets/styles/vars'; 2 | 3 | :global { 4 | 5 | $particles: 100; 6 | $width: 500; 7 | $height: 500; 8 | 9 | @media #{$mobile} { 10 | $particles: 30; 11 | } 12 | 13 | // Create the explosion... 14 | $box-shadow: (); 15 | $box-shadow2: (); 16 | @for $i from 0 through $particles { 17 | $box-shadow: $box-shadow, 18 | random($width) - $width / 2 + px 19 | random($height) - $height / 1.2 + px 20 | hsl(random(360), 100, 50); 21 | $box-shadow2: $box-shadow2, 0 0 #fff; 22 | } 23 | 24 | .firework > .before, 25 | .firework > .after { 26 | animation: 1s bang ease-out infinite backwards, 27 | 1s gravity ease-in infinite backwards, 5s position linear infinite backwards; 28 | border-radius: 50%; 29 | box-shadow: $box-shadow2; 30 | height: 5px; 31 | position: absolute; 32 | width: 5px; 33 | } 34 | 35 | .firework > .after { 36 | // animation-delay: 10s, 10s, 10s; 37 | animation-duration: 1.25s, 1.25s, 6.25s; 38 | } 39 | 40 | /* stylelint-disable max-nesting-depth */ 41 | @keyframes :global(bang) { 42 | to { 43 | box-shadow: $box-shadow; 44 | } 45 | } 46 | 47 | @keyframes :global(gravity) { 48 | to { 49 | opacity: 0; 50 | transform: translateY(200px); 51 | } 52 | } 53 | 54 | @keyframes :global(position) { 55 | 0%, 56 | 19.9% { 57 | margin-left: 40%; 58 | margin-top: 10%; 59 | } 60 | 61 | 20%, 62 | 39.9% { 63 | margin-left: 30%; 64 | margin-top: 40%; 65 | } 66 | 67 | 40%, 68 | 59.9% { 69 | margin-left: 70%; 70 | margin-top: 20%; 71 | } 72 | 73 | 60%, 74 | 79.9% { 75 | margin-left: 20%; 76 | margin-top: 30%; 77 | } 78 | 79 | 80%, 80 | 99.9% { 81 | margin-left: 80%; 82 | margin-top: 30%; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/components/Firework/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import PropTypes from 'prop-types'; 3 | import './firework.scss'; 4 | 5 | // Display firework animation when game over 6 | export default class Firework extends React.Component { 7 | // Render once, as no props and state 8 | shouldComponentUpdate() { 9 | return false; 10 | } 11 | 12 | render() { 13 | return ( 14 |
15 |
16 |
17 |
18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Footer/__tests__/Footer.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Enzyme, { shallow } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import Footer from '..'; 6 | 7 | Enzyme.configure({ adapter: new Adapter() }); 8 | 9 | describe('