├── __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 |

23 |
24 | React (anyhow) when Thanos snaps his finger.{" "}
25 | Click the gauntlet to see it in action
26 |
27 |
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 |
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 |
--------------------------------------------------------------------------------