├── public ├── icon128.png ├── icon16.png ├── icon32.png ├── icon64.png ├── popup.html └── manifest.json ├── src ├── custom.d.ts ├── App.tsx ├── popup.tsx ├── types.ts ├── components │ └── Button │ │ ├── Button.css │ │ └── Button.tsx ├── popup.css ├── content.ts ├── background.ts └── content.css ├── .babelrc ├── .gitignore ├── tsconfig.json ├── readme.md ├── package.json └── webpack.config.js /public/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sivertschou/snow-extension/HEAD/public/icon128.png -------------------------------------------------------------------------------- /public/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sivertschou/snow-extension/HEAD/public/icon16.png -------------------------------------------------------------------------------- /public/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sivertschou/snow-extension/HEAD/public/icon32.png -------------------------------------------------------------------------------- /public/icon64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sivertschou/snow-extension/HEAD/public/icon64.png -------------------------------------------------------------------------------- /src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: any; 3 | export default content; 4 | } -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Button } from "./components/Button/Button"; 3 | 4 | const App = () => { 5 | return 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/content.ts: -------------------------------------------------------------------------------- 1 | import "./content.css"; 2 | import { MessageTypes } from "./types"; 3 | 4 | const body = document.getElementsByTagName("body"); 5 | 6 | const snowflakesContainer = document.createElement("div"); 7 | snowflakesContainer.className = "snowflakes"; 8 | snowflakesContainer.setAttribute("aria-hidden", "true"); 9 | 10 | const snowflake = document.createElement("div"); 11 | snowflake.className = "snowflake"; 12 | snowflake.innerHTML = "❆"; 13 | 14 | for (let i = 0; i < 12; i++) { 15 | snowflakesContainer.appendChild(snowflake.cloneNode(true)); 16 | } 17 | 18 | chrome.runtime.sendMessage({ type: "REQ_SNOW_STATUS" }); 19 | 20 | let snowing = false; 21 | chrome.runtime.onMessage.addListener((message: MessageTypes) => { 22 | switch (message.type) { 23 | case "SNOW_STATUS": 24 | if (message.snowing) { 25 | if (!snowing) { 26 | body[0]?.prepend(snowflakesContainer); 27 | } 28 | } else { 29 | snowflakesContainer.parentNode?.removeChild(snowflakesContainer); 30 | } 31 | snowing = message.snowing; 32 | break; 33 | default: 34 | break; 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | import { MessageTypes } from "./types"; 2 | 3 | const sendSnowStatus = (snowing: boolean) => { 4 | const message = { type: "SNOW_STATUS", snowing }; 5 | 6 | // send message to popup 7 | chrome.runtime.sendMessage(message); 8 | 9 | // send message to every active tab 10 | chrome.tabs.query({}, (tabs) => { 11 | tabs.forEach((tab) => { 12 | if (tab.id) { 13 | chrome.tabs.sendMessage(tab.id, message); 14 | } 15 | }); 16 | }); 17 | }; 18 | 19 | let snowing = false; 20 | 21 | // Get locally stored value 22 | chrome.storage.local.get("snowing", (res) => { 23 | if (res["snowing"]) { 24 | snowing = true; 25 | } else { 26 | snowing = false; 27 | } 28 | }); 29 | 30 | chrome.runtime.onMessage.addListener((message: MessageTypes) => { 31 | switch (message.type) { 32 | case "REQ_SNOW_STATUS": 33 | sendSnowStatus(snowing); 34 | break; 35 | case "TOGGLE_SNOW": 36 | snowing = message.snowing; 37 | chrome.storage.local.set({ snowing: snowing }); 38 | sendSnowStatus(snowing); 39 | break; 40 | default: 41 | break; 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-extension-react-typescript-boilerplate", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "build": "webpack", 8 | "start": "webpack --watch" 9 | }, 10 | "keywords": [], 11 | "author": "sivertschou", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@babel/core": "^7.12.3", 15 | "@babel/preset-env": "^7.12.1", 16 | "@babel/preset-react": "^7.12.1", 17 | "@hot-loader/react-dom": "^17.0.0-rc.2", 18 | "@types/chrome": "0.0.125", 19 | "@types/react": "^16.9.53", 20 | "@types/react-dom": "^16.9.8", 21 | "babel-loader": "^8.1.0", 22 | "copy-webpack-plugin": "^6.2.1", 23 | "css-loader": "^5.0.0", 24 | "file-loader": "^6.1.1", 25 | "style-loader": "^2.0.0", 26 | "ts-loader": "^8.0.5", 27 | "typescript": "^4.0.3", 28 | "url-loader": "^4.1.1", 29 | "webpack": "^5.1.3", 30 | "webpack-cli": "^4.0.0", 31 | "webpack-dev-server": "^4.1.1" 32 | }, 33 | "dependencies": { 34 | "react": "^16.14.0", 35 | "react-dom": "^16.14.0", 36 | "react-hot-loader": "^4.13.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | const CopyPlugin = require("copy-webpack-plugin"); 4 | 5 | const config = { 6 | entry: { 7 | popup: path.join(__dirname, "src/popup.tsx"), 8 | content: path.join(__dirname, "src/content.ts"), 9 | background: path.join(__dirname, "src/background.ts"), 10 | }, 11 | output: { path: path.join(__dirname, "dist"), filename: "[name].js" }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.(js|jsx)$/, 16 | use: "babel-loader", 17 | exclude: /node_modules/, 18 | }, 19 | { 20 | test: /\.css$/, 21 | use: ["style-loader", "css-loader"], 22 | exclude: /\.module\.css$/, 23 | }, 24 | { 25 | test: /\.ts(x)?$/, 26 | loader: "ts-loader", 27 | exclude: /node_modules/, 28 | }, 29 | { 30 | test: /\.css$/, 31 | use: [ 32 | "style-loader", 33 | { 34 | loader: "css-loader", 35 | options: { 36 | importLoaders: 1, 37 | modules: true, 38 | }, 39 | }, 40 | ], 41 | include: /\.module\.css$/, 42 | }, 43 | { 44 | test: /\.svg$/, 45 | use: "file-loader", 46 | }, 47 | { 48 | test: /\.png$/, 49 | use: [ 50 | { 51 | loader: "url-loader", 52 | options: { 53 | mimetype: "image/png", 54 | }, 55 | }, 56 | ], 57 | }, 58 | ], 59 | }, 60 | resolve: { 61 | extensions: [".js", ".jsx", ".tsx", ".ts"], 62 | alias: { 63 | "react-dom": "@hot-loader/react-dom", 64 | }, 65 | }, 66 | devServer: { 67 | contentBase: "./dist", 68 | }, 69 | plugins: [ 70 | new CopyPlugin({ 71 | patterns: [{ from: "public", to: "." }], 72 | }), 73 | ], 74 | }; 75 | 76 | module.exports = config; 77 | -------------------------------------------------------------------------------- /src/content.css: -------------------------------------------------------------------------------- 1 | /* customizable snowflake styling */ 2 | .snowflake { 3 | color: #fff; 4 | font-size: 1em; 5 | font-family: Arial, sans-serif; 6 | text-shadow: 0 0 5px #000; 7 | pointer-events: none; 8 | } 9 | 10 | @-webkit-keyframes snowflakes-fall { 11 | 0% { 12 | top: -10%; 13 | } 14 | 100% { 15 | top: 100%; 16 | } 17 | } 18 | @-webkit-keyframes snowflakes-shake { 19 | 0%, 20 | 100% { 21 | -webkit-transform: translateX(0); 22 | transform: translateX(0); 23 | } 24 | 50% { 25 | -webkit-transform: translateX(80px); 26 | transform: translateX(80px); 27 | } 28 | } 29 | @keyframes snowflakes-fall { 30 | 0% { 31 | top: -10%; 32 | } 33 | 100% { 34 | top: 100%; 35 | } 36 | } 37 | @keyframes snowflakes-shake { 38 | 0%, 39 | 100% { 40 | transform: translateX(0); 41 | } 42 | 50% { 43 | transform: translateX(80px); 44 | } 45 | } 46 | .snowflake { 47 | position: fixed; 48 | top: -10%; 49 | z-index: 9999; 50 | -webkit-user-select: none; 51 | -moz-user-select: none; 52 | -ms-user-select: none; 53 | user-select: none; 54 | cursor: default; 55 | -webkit-animation-name: snowflakes-fall, snowflakes-shake; 56 | -webkit-animation-duration: 10s, 3s; 57 | -webkit-animation-timing-function: linear, ease-in-out; 58 | -webkit-animation-iteration-count: infinite, infinite; 59 | -webkit-animation-play-state: running, running; 60 | animation-name: snowflakes-fall, snowflakes-shake; 61 | animation-duration: 10s, 3s; 62 | animation-timing-function: linear, ease-in-out; 63 | animation-iteration-count: infinite, infinite; 64 | animation-play-state: running, running; 65 | } 66 | .snowflake:nth-of-type(0) { 67 | left: 1%; 68 | -webkit-animation-delay: 0s, 0s; 69 | animation-delay: 0s, 0s; 70 | } 71 | .snowflake:nth-of-type(1) { 72 | left: 10%; 73 | -webkit-animation-delay: 1s, 1s; 74 | animation-delay: 1s, 1s; 75 | } 76 | .snowflake:nth-of-type(2) { 77 | left: 20%; 78 | -webkit-animation-delay: 6s, 0.5s; 79 | animation-delay: 6s, 0.5s; 80 | } 81 | .snowflake:nth-of-type(3) { 82 | left: 30%; 83 | -webkit-animation-delay: 4s, 2s; 84 | animation-delay: 4s, 2s; 85 | } 86 | .snowflake:nth-of-type(4) { 87 | left: 40%; 88 | -webkit-animation-delay: 2s, 2s; 89 | animation-delay: 2s, 2s; 90 | } 91 | .snowflake:nth-of-type(5) { 92 | left: 50%; 93 | -webkit-animation-delay: 8s, 3s; 94 | animation-delay: 8s, 3s; 95 | } 96 | .snowflake:nth-of-type(6) { 97 | left: 60%; 98 | -webkit-animation-delay: 6s, 2s; 99 | animation-delay: 6s, 2s; 100 | } 101 | .snowflake:nth-of-type(7) { 102 | left: 70%; 103 | -webkit-animation-delay: 2.5s, 1s; 104 | animation-delay: 2.5s, 1s; 105 | } 106 | .snowflake:nth-of-type(8) { 107 | left: 80%; 108 | -webkit-animation-delay: 1s, 0s; 109 | animation-delay: 1s, 0s; 110 | } 111 | .snowflake:nth-of-type(9) { 112 | left: 90%; 113 | -webkit-animation-delay: 3s, 1.5s; 114 | animation-delay: 3s, 1.5s; 115 | } 116 | .snowflake:nth-of-type(10) { 117 | left: 25%; 118 | -webkit-animation-delay: 2s, 0s; 119 | animation-delay: 2s, 0s; 120 | } 121 | .snowflake:nth-of-type(11) { 122 | left: 65%; 123 | -webkit-animation-delay: 4s, 2.5s; 124 | animation-delay: 4s, 2.5s; 125 | } 126 | --------------------------------------------------------------------------------