├── 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 ;
6 | };
7 |
8 | export default App;
9 |
--------------------------------------------------------------------------------
/src/popup.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as ReactDOM from "react-dom";
3 |
4 | import App from "./App";
5 | import "./popup.css";
6 |
7 | var mountNode = document.getElementById("popup");
8 | ReactDOM.render(, mountNode);
9 |
--------------------------------------------------------------------------------
/public/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Chrome Popup
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/ .babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "modules": false
7 | }
8 | ],
9 | "@babel/preset-react"
10 | ],
11 | "plugins": [
12 | "react-hot-loader/babel"
13 | ]
14 | }
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | interface SnowRequest {
2 | type: "REQ_SNOW_STATUS";
3 | }
4 |
5 | interface SnowResponse {
6 | type: "SNOW_STATUS";
7 | snowing: boolean;
8 | }
9 |
10 | interface SnowToggle {
11 | type: "TOGGLE_SNOW";
12 | snowing: boolean;
13 | }
14 |
15 | export type MessageTypes = SnowRequest | SnowResponse | SnowToggle;
16 |
--------------------------------------------------------------------------------
/src/components/Button/Button.css:
--------------------------------------------------------------------------------
1 | .snowButton {
2 | border: 0;
3 | background-color: #1a1d22;
4 | color: white;
5 | padding: 5px 10px;
6 | width: 100%;
7 | cursor: pointer;
8 | }
9 |
10 | .buttonContainer {
11 | display: flex;
12 | background-color: #282c34;
13 | color: white;
14 | min-width: 150px;
15 | padding: 10px;
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 | /dist
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/src/popup.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "sourceMap": true,
5 | "strict": true,
6 | "noImplicitReturns": true,
7 | "noImplicitAny": true,
8 | "module": "es6",
9 | "moduleResolution": "node",
10 | "target": "es5",
11 | "allowJs": true,
12 | "jsx": "react",
13 | },
14 | "include": [
15 | "./src/**/*",
16 | "src/custom.d.ts"
17 | ]
18 | }
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Snow Extension",
3 | "description": "Chrome Extension for making it snow in your browser!",
4 | "manifest_version": 2,
5 | "version": "1.0.0",
6 | "icons": {
7 | "16": "icon16.png",
8 | "32": "icon32.png",
9 | "64": "icon64.png",
10 | "128": "icon128.png"
11 | },
12 | "browser_action": {
13 | "default_icon": "icon128.png",
14 | "default_popup": "popup.html"
15 | },
16 | "background": {
17 | "scripts": [
18 | "background.js"
19 | ],
20 | "persistent": true
21 | },
22 | "content_scripts": [
23 | {
24 | "matches": [
25 | "*://*/*"
26 | ],
27 | "js": [
28 | "content.js"
29 | ]
30 | }
31 | ],
32 | "permissions": [
33 | "storage"
34 | ]
35 | }
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Snow Chrome Extension
2 |
3 | A small extension for making the browser a bit more christmassy ❄️
4 |
5 | 
6 |
7 | ## Installation
8 |
9 | Navigate to the project directory and install the dependencies.
10 |
11 | ```
12 | $ npm install
13 | ```
14 |
15 | To build the extension, and rebuild it when the files are changed, run
16 |
17 | ```
18 | $ npm start
19 | ```
20 |
21 | After the project has been built, a directory named `dist` has been created. You have to add this directory to your Chrome browser:
22 |
23 | 1. Open Chrome.
24 | 2. Navigate to `chrome://extensions`.
25 | 3. Enable _Developer mode_.
26 | 4. Click _Load unpacked_.
27 | 5. Select the `dist` directory.
28 |
--------------------------------------------------------------------------------
/src/components/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { MessageTypes } from "../../types";
3 | import "./Button.css";
4 |
5 | export const Button = () => {
6 | const [snowing, setSnowing] = React.useState(true);
7 |
8 | React.useEffect(() => {
9 | chrome.runtime.sendMessage({ type: "REQ_SNOW_STATUS" });
10 |
11 | chrome.runtime.onMessage.addListener((message: MessageTypes) => {
12 | switch (message.type) {
13 | case "SNOW_STATUS":
14 | setSnowing(message.snowing);
15 | break;
16 | default:
17 | break;
18 | }
19 | });
20 | }, []);
21 |
22 | const onClick = () => {
23 | chrome.runtime.sendMessage({ type: "TOGGLE_SNOW", snowing: !snowing });
24 | };
25 |
26 | return (
27 |
28 |
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 |
--------------------------------------------------------------------------------