├── __mocks__ └── fileMock.js ├── .babelrc ├── lib ├── assets │ ├── thanos_snap.png │ ├── thanos_time.png │ ├── thanos_snap_sound.mp3 │ └── thanos_reverse_sound.mp3 ├── style.css └── index.js ├── .editorconfig ├── examples ├── public │ └── index.html ├── package.json └── src │ ├── index.js │ ├── style.css │ └── assets │ └── logo.svg ├── rollup.config.js ├── license ├── test └── lib.spec.js ├── .gitignore ├── readme.md └── package.json /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = ''; 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } 4 | -------------------------------------------------------------------------------- /lib/assets/thanos_snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luqmanoop/react-thanos/HEAD/lib/assets/thanos_snap.png -------------------------------------------------------------------------------- /lib/assets/thanos_time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luqmanoop/react-thanos/HEAD/lib/assets/thanos_time.png -------------------------------------------------------------------------------- /lib/assets/thanos_snap_sound.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luqmanoop/react-thanos/HEAD/lib/assets/thanos_snap_sound.mp3 -------------------------------------------------------------------------------- /lib/assets/thanos_reverse_sound.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luqmanoop/react-thanos/HEAD/lib/assets/thanos_reverse_sound.mp3 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /examples/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Thanos 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/style.css: -------------------------------------------------------------------------------- 1 | .thanos { 2 | height: 80px; 3 | width: 80px; 4 | position: relative; 5 | cursor: pointer; 6 | } 7 | 8 | .gauntlet { 9 | width: 80px; 10 | height: 80px; 11 | position: absolute; 12 | } 13 | 14 | .gauntlet.snap { 15 | background: url("./assets/thanos_snap.png") 16 | no-repeat 0 0; 17 | animation-delay: 0.5s; 18 | } 19 | 20 | .gauntlet.snap-reverse { 21 | background: url("./assets/thanos_time.png") 22 | no-repeat 0 0; 23 | } 24 | 25 | .animate { 26 | animation: gauntlet 1.5s steps(48); 27 | } 28 | 29 | @keyframes gauntlet { 30 | 0% { 31 | background-position: 0px 0px; 32 | } 33 | 100% { 34 | background-position: -3840px 0px; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.8.6", 7 | "react-dom": "^16.8.6", 8 | "react-scripts": "^4.0.3", 9 | "react-thanos": "^2.0.0" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "eject": "react-scripts eject" 16 | }, 17 | "eslintConfig": { 18 | "extends": "react-app" 19 | }, 20 | "browserslist": { 21 | "production": [ 22 | ">0.2%", 23 | "not dead", 24 | "not op_mini all" 25 | ], 26 | "development": [ 27 | "last 1 chrome version", 28 | "last 1 firefox version", 29 | "last 1 safari version" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Thanos } from "react-thanos"; 4 | 5 | import logo from "./assets/logo.svg"; 6 | 7 | import "./style.css"; 8 | 9 | const App = () => { 10 | const [snap, setSnap] = useState(null); 11 | 12 | return ( 13 |
14 |

15 | React 16 | setSnap(true)} 18 | onSnapReverse={() => setSnap(false)} 19 | />{" "} 20 | Thanos 21 |

22 | react logo 23 |

24 | React (anyhow) when Thanos snaps his finger.{" "} 25 | Click the gauntlet to see it in action 26 |

27 |
28 | 29 | View it on GitHub 30 | 31 |
32 |
33 |
34 | ); 35 | }; 36 | 37 | ReactDOM.render(, document.getElementById("root")); 38 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { babel } from "@rollup/plugin-babel"; 2 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 3 | import rebase from "rollup-plugin-rebase"; 4 | import peerDepsExternal from "rollup-plugin-peer-deps-external"; 5 | import transformRuntime from "@babel/plugin-transform-runtime"; 6 | import presetEnv from "@babel/preset-env"; 7 | import presetReact from "@babel/preset-react"; 8 | 9 | import pkg from "./package.json"; 10 | 11 | const sourcemap = false; 12 | 13 | export default { 14 | input: "lib/index.js", 15 | plugins: [ 16 | rebase({ 17 | assetFolder: "assets", 18 | }), 19 | nodeResolve(), 20 | babel({ 21 | babelrc: false, 22 | babelHelpers: "runtime", 23 | presets: [presetEnv, presetReact], 24 | plugins: [transformRuntime], 25 | }), 26 | peerDepsExternal(), 27 | 28 | ], 29 | output: [ 30 | { 31 | file: pkg.module, 32 | format: "es", 33 | sourcemap, 34 | }, 35 | { 36 | file: pkg.main, 37 | format: "cjs", 38 | sourcemap, 39 | }, 40 | ], 41 | }; 42 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Luqman Olushi O. 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 | -------------------------------------------------------------------------------- /examples/src/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | background-color: #333; 9 | } 10 | 11 | .app { 12 | font-family: "Open Sans", Verdana, sans-serif; 13 | padding: 20px 20px 0; 14 | max-width: 960px; 15 | margin: 50px auto 0; 16 | font-size: 18px; 17 | border: 2px solid #333; 18 | color: #333; 19 | transition: all 0.55s; 20 | text-align: center; 21 | } 22 | 23 | .dark { 24 | background-color: #131516; 25 | color: #eff8f3; 26 | } 27 | .dark a { 28 | color: #00ff80; 29 | } 30 | 31 | .light { 32 | color: #0f2417; 33 | background: #fff; 34 | } 35 | 36 | h1 { 37 | display: flex; 38 | justify-content: center; 39 | margin-top: 10px; 40 | } 41 | 42 | a { 43 | display: inline-block; 44 | margin-top: 8px; 45 | } 46 | 47 | .thanos { 48 | position: relative; 49 | top: -20px; 50 | } 51 | 52 | .logo { 53 | height: 40vmin; 54 | pointer-events: none; 55 | width: 100%; 56 | } 57 | 58 | @media (prefers-reduced-motion: no-preference) { 59 | .logo { 60 | animation: logo-spin infinite 20s linear; 61 | } 62 | } 63 | 64 | @keyframes logo-spin { 65 | from { 66 | transform: rotate(0deg); 67 | } 68 | to { 69 | transform: rotate(360deg); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test/lib.spec.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, fireEvent, waitFor } from "@testing-library/react"; 3 | import "regenerator-runtime"; 4 | import "@testing-library/jest-dom"; 5 | 6 | import { Thanos } from "../lib"; 7 | 8 | window.HTMLMediaElement.prototype.play = () => {}; 9 | 10 | const onSnapMock = jest.fn(); 11 | const onSnapReverseMock = jest.fn() 12 | 13 | const App = () => { 14 | return ; 15 | }; 16 | 17 | describe("Gauntlet", () => { 18 | it("callbacks onSnap", async () => { 19 | const { findByTestId } = render(); 20 | const gauntlet = await findByTestId("gauntlet"); 21 | fireEvent.click(await findByTestId("gauntlet")); 22 | fireEvent.animationEnd(gauntlet); 23 | expect(gauntlet.classList.contains('snap')).toBeTruthy(); 24 | expect(onSnapMock).toHaveBeenCalledTimes(1); 25 | }); 26 | 27 | it("callbacks onSnapReverseMock", async () => { 28 | const { findByTestId } = render(); 29 | const gauntlet = await findByTestId("gauntlet"); 30 | // snap 31 | fireEvent.click(await findByTestId("gauntlet")); 32 | fireEvent.animationEnd(gauntlet); 33 | // undo snap 34 | fireEvent.click(await findByTestId("gauntlet")); 35 | fireEvent.animationEnd(gauntlet); 36 | 37 | expect(gauntlet.classList.contains('snap-reverse')).toBeTruthy(); 38 | expect(onSnapReverseMock).toHaveBeenCalledTimes(1); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | .env.test 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # DynamoDB Local files 84 | .dynamodb/ 85 | 86 | dist 87 | build 88 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState, useEffect } from "react"; 2 | import snapSoundEffect from "./assets/thanos_snap_sound.mp3"; 3 | import snapReverseSoundEffect from "./assets/thanos_reverse_sound.mp3"; 4 | import "./style.css"; 5 | 6 | export const Thanos = ({ onSnap, onSnapReverse }) => { 7 | const [animating, setAnimating] = useState(false); 8 | const ref = useRef(); 9 | const snapSound = new Audio(snapSoundEffect); 10 | const snapReverseSound = new Audio(snapReverseSoundEffect); 11 | 12 | const addSoundEffect = (snapSoundEffect) => { 13 | if (snapSoundEffect) { 14 | snapSound.play(); 15 | } else { 16 | snapReverseSound.play(); 17 | } 18 | }; 19 | 20 | useEffect(() => { 21 | const thanos = ref.current; 22 | const handleAnimationEnd = (e) => { 23 | if (e.target.classList.contains("snap")) { 24 | onSnap(); 25 | } else onSnapReverse(); 26 | e.target.classList.remove("animate"); 27 | setAnimating(false); 28 | }; 29 | 30 | thanos.addEventListener("animationend", handleAnimationEnd); 31 | 32 | return () => { 33 | if (thanos) { 34 | thanos.removeEventListener("animationend", handleAnimationEnd); 35 | } 36 | }; 37 | }, []); 38 | 39 | const handleClick = (e) => { 40 | if (animating) return; 41 | 42 | setAnimating(true); 43 | 44 | const gauntlet = e.target; 45 | const snapSoundEffect = gauntlet.classList.toggle("snap"); 46 | 47 | gauntlet.classList.toggle("snap-reverse"); 48 | gauntlet.classList.add("animate"); 49 | 50 | addSoundEffect(snapSoundEffect); 51 | }; 52 | 53 | return ( 54 |
55 |
56 |
57 | ); 58 | }; 59 | 60 | Thanos.defaultProps = { 61 | onSnap: () => {}, 62 | onSnapReverse: () => {}, 63 | }; 64 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

react-thanos

4 |

React hooks implementation of Google's Thanos easter egg

5 |
6 | 7 | ## Prerequisite 8 | - [NodeJS](https://nodejs.org) 9 | - Project running >=[React **16.8**](https://reactjs.org/blog/2019/02/06/react-v16.8.0.html) 10 | ## Installation 📦 11 | 12 | ```bash 13 | npm install react-thanos 14 | ``` 15 | 16 | or with yarn 17 | 18 | ```bash 19 | yarn add react-thanos 20 | ``` 21 | 22 | ## Usage 23 | 24 | ```javascript 25 | import { Thanos } from "react-thanos"; 26 | 27 | console.log("I love you 3000! Decimate...") } 29 | onSnapReverse={() => console.log("Avengers assemble!") } 30 | /> 31 | ``` 32 | 33 | ## Examples 34 | See [examples](https://github.com/codeshifu/react-thanos/tree/master/examples) folder 35 | 36 | Live demo https://react-thanos.netlify.com/ 37 | 38 | ## API (props) 39 | 40 | ### onSnap() 41 | 42 | Type: `function` 43 | 44 | Called after Thanos snaps his finger 45 | 46 | ### onSnapReverse() 47 | 48 | Type: `function` 49 | 50 | Called after Thanos undo/reverse snap 51 | 52 | 53 | ## Inspiration 54 | This project was inspired by the famous Google's [Thanos easter egg](http://google.com/search?q=thanos) released 55 | shortly after [Avengers: Endgame](https://www.imdb.com/title/tt4154796/) premiered in cinemas. 56 | 57 | ## Credits 58 | [Assets](https://github.com/codeshifu/react-thanos/tree/master/lib/assets) 59 | used in this project were downloaded from Google. I own no rights to them. 60 | 61 | ## Contributing 62 | Feel free to send in contributions of any kind. All are welcome 🙂 63 | 64 | ## License 65 | **react-thanos** is licensed under [MIT](https://github.com/codeshifu/react-thanos/blob/master/license) 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-thanos", 3 | "version": "2.0.0", 4 | "description": "React hooks implementation of Google's \"Thanos\" easter egg", 5 | "directories": { 6 | "lib": "lib" 7 | }, 8 | "main": "dist/index.js", 9 | "module": "dist/index.es.js", 10 | "source": "lib/index.js", 11 | "scripts": { 12 | "clean": "rimraf dist", 13 | "bundle": "rollup -c", 14 | "build": "npm-run-all clean bundle", 15 | "release": "npm run build && np", 16 | "test": "jest" 17 | }, 18 | "engines": { 19 | "node": ">=15.0.0" 20 | }, 21 | "files": [ 22 | "dist", 23 | "readme.md", 24 | "license.md" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/codeshifu/react-thanos.git" 29 | }, 30 | "keywords": [ 31 | "thanos", 32 | "snap", 33 | "gauntlet", 34 | "decimation", 35 | "react", 36 | "google" 37 | ], 38 | "author": "Luqman Olushi (https://codeshifu.github.io/)", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/codeshifu/react-thanos/issues" 42 | }, 43 | "homepage": "https://github.com/codeshifu/react-thanos#readme", 44 | "devDependencies": { 45 | "@babel/cli": "7.15.7", 46 | "@babel/core": "7.4.4", 47 | "@babel/plugin-transform-runtime": "7.15.8", 48 | "@babel/preset-env": "7.4.4", 49 | "@babel/preset-react": "7.0.0", 50 | "@rollup/plugin-babel": "5.3.0", 51 | "@rollup/plugin-node-resolve": "13.0.5", 52 | "@testing-library/jest-dom": "^5.14.1", 53 | "@testing-library/react": "^12.1.2", 54 | "babel-jest": "^26.6.3", 55 | "jest": "^27.2.5", 56 | "np": "7.5.0", 57 | "npm-run-all": "4.1.5", 58 | "react": "16.9", 59 | "react-dom": "16.9", 60 | "react-test-renderer": "16.8", 61 | "regenerator-runtime": "^0.13.9", 62 | "rimraf": "2.6.3", 63 | "rollup": "2.58.0", 64 | "rollup-plugin-peer-deps-external": "2.2.4", 65 | "rollup-plugin-rebase": "3.6.9" 66 | }, 67 | "peerDependencies": { 68 | "react": "16.9", 69 | "react-dom": "16.9" 70 | }, 71 | "jest": { 72 | "moduleNameMapper": { 73 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", 74 | "\\.(css|less)$": "/__mocks__/fileMock.js" 75 | }, 76 | "testEnvironment": "jsdom" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /examples/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------