├── .gitignore ├── LICENSE ├── README.md ├── assets ├── Github.svg ├── Icon.svg ├── StoreIcon.png ├── StoreIconDark.png ├── StoreScreenshot.png ├── StoreScreenshot1280x800.png └── StoreScreenshotScaled.png ├── jest.config.js ├── package.json ├── public ├── icon.png ├── manifest.json ├── options.html └── popup.html ├── src ├── content_script.tsx ├── mount_view.tsx ├── options.tsx ├── popup.css ├── popup.tsx └── types.d.ts ├── tsconfig.json └── webpack ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules/ 3 | dist/ 4 | tmp/ 5 | .vscode 6 | .github 7 | 8 | 9 | .DS_Store 10 | .DS_Store? 11 | ._* 12 | .Spotlight-V100 13 | .Trashes 14 | ehthumbs.db 15 | Thumbs.db 16 | 17 | dist.zip 18 | dist.pem 19 | dist.crx -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tomofumi Chiba 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Skip the Scroll 2 | 3 | 4 | 5 | Tired of scrolling through hundreds of github issue comments to find that one magical response? Skip the scroll is a simple chrome extension that sorts github issue comments for you and allows you to iterate through them based on their social reactions. 6 | 7 | [Get extension on the Chrome Web store](https://chrome.google.com/webstore/detail/skip-the-scroll/mfehannpjmgfagldoilpngeoecdfgmnd) 8 | 9 | ## Prerequisites 10 | 11 | - [node + npm](https://nodejs.org/) (Current Version) 12 | 13 | ## Setup 14 | 15 | ``` 16 | npm install 17 | ``` 18 | 19 | ## Build 20 | 21 | ``` 22 | npm run build 23 | ``` 24 | 25 | ## Build in watch mode 26 | 27 | ### terminal 28 | 29 | ``` 30 | npm run watch 31 | ``` 32 | -------------------------------------------------------------------------------- /assets/Github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 26 | 27 | -------------------------------------------------------------------------------- /assets/Icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /assets/StoreIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henryjeff/skip-the-scroll/ca76d6701de3df3a62d285024e89f18440793602/assets/StoreIcon.png -------------------------------------------------------------------------------- /assets/StoreIconDark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henryjeff/skip-the-scroll/ca76d6701de3df3a62d285024e89f18440793602/assets/StoreIconDark.png -------------------------------------------------------------------------------- /assets/StoreScreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henryjeff/skip-the-scroll/ca76d6701de3df3a62d285024e89f18440793602/assets/StoreScreenshot.png -------------------------------------------------------------------------------- /assets/StoreScreenshot1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henryjeff/skip-the-scroll/ca76d6701de3df3a62d285024e89f18440793602/assets/StoreScreenshot1280x800.png -------------------------------------------------------------------------------- /assets/StoreScreenshotScaled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henryjeff/skip-the-scroll/ca76d6701de3df3a62d285024e89f18440793602/assets/StoreScreenshotScaled.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "src" 4 | ], 5 | "transform": { 6 | "^.+\\.ts$": "ts-jest" 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "skip-the-scroll", 3 | "version": "1.0.0", 4 | "description": "skip-the-scroll", 5 | "main": "index.js", 6 | "scripts": { 7 | "watch": "webpack --config webpack/webpack.dev.js --watch", 8 | "build": "webpack --config webpack/webpack.prod.js", 9 | "clean": "rimraf dist", 10 | "test": "npx jest", 11 | "style": "prettier --write \"src/**/*.{ts,tsx}\"" 12 | }, 13 | "author": "", 14 | "license": "MIT", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/henryjeff/skip-the-scroll" 18 | }, 19 | "dependencies": { 20 | "@svgr/webpack": "^6.1.2", 21 | "framer-motion": "^5.5.5", 22 | "react": "^17.0.1", 23 | "react-dom": "^17.0.1", 24 | "svg-url-loader": "^7.1.1" 25 | }, 26 | "devDependencies": { 27 | "@types/chrome": "0.0.158", 28 | "@types/jest": "^27.0.2", 29 | "@types/react": "^17.0.0", 30 | "@types/react-dom": "^17.0.0", 31 | "copy-webpack-plugin": "^9.0.1", 32 | "css-loader": "^6.5.1", 33 | "glob": "^7.1.6", 34 | "jest": "^27.2.1", 35 | "prettier": "^2.2.1", 36 | "rimraf": "^3.0.2 ", 37 | "style-loader": "^3.3.1", 38 | "ts-jest": "^27.0.5", 39 | "ts-loader": "^8.0.0", 40 | "typescript": "^4.4.3 ", 41 | "webpack": "^5.0.0", 42 | "webpack-cli": "^4.0.0", 43 | "webpack-merge": "^5.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henryjeff/skip-the-scroll/ca76d6701de3df3a62d285024e89f18440793602/public/icon.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | 4 | "name": "Skip the Scroll", 5 | "description": "Tired of scrolling through hundreds of Github issue comments to find that one magical response? Skip the scroll is here to help!", 6 | "version": "1.0.0", 7 | 8 | "options_ui": { 9 | "page": "options.html" 10 | }, 11 | 12 | "action": { 13 | "default_icon": "icon.png", 14 | "default_popup": "popup.html" 15 | }, 16 | 17 | "content_scripts": [ 18 | { 19 | "matches": ["https://github.com/*"], 20 | "js": ["js/vendor.js", "js/content_script.js"] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /public/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Skip the Scroll Options 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /public/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Skip the Scroll 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/content_script.tsx: -------------------------------------------------------------------------------- 1 | let comments: CommentData[] = []; 2 | 3 | chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) { 4 | if (msg.type === "loadComments") { 5 | const _comments = parseComments(); 6 | comments = _comments; 7 | sendResponse(comments); 8 | return true; 9 | } 10 | if (msg.type === "scrollTo") { 11 | const commentData = comments[Number(msg.index)]; 12 | if (commentData) { 13 | const y = 14 | commentData.comment.getBoundingClientRect().top + 15 | window.pageYOffset + 16 | -128; 17 | window.scrollTo({ top: y, behavior: "smooth" }); 18 | comments.forEach((comment) => { 19 | // @ts-ignore 20 | comment.comment.style = "box-shadow: 0px 0px 0px 0px"; 21 | }); 22 | // @ts-ignore 23 | commentData.comment.style = "box-shadow: 0px 0px 0px 1.5px #2BA44E;"; 24 | sendResponse(commentData); 25 | return true; 26 | } 27 | } 28 | return true; 29 | }); 30 | 31 | type CommentData = { 32 | comment: Element; 33 | upvotes: number; 34 | timestamp: number; 35 | }; 36 | 37 | const parseComments = () => { 38 | const comments: CommentData[] = []; 39 | 40 | // Get all posts on the page 41 | const commentElements = document.getElementsByClassName( 42 | "timeline-comment comment" 43 | ); 44 | 45 | // All upvote emojis 46 | const upvoteEmojis = ["👍", "😄", "🎉", "🚀", "❤️"]; 47 | 48 | Array.from(commentElements).forEach((comment) => { 49 | const reactions = comment.getElementsByClassName( 50 | "social-reaction-summary-item" 51 | ); 52 | let totalUpvotes = 0; 53 | Array.from(reactions).forEach((reaction) => { 54 | // @ts-ignore 55 | let splitText: string[] = reaction.innerText.split("\n"); 56 | 57 | let emoji = splitText[0]; 58 | let numReactions = Number(splitText[1]); 59 | 60 | // Increase totalUpvotes if there are upvote emojis in the reaction 61 | if (upvoteEmojis.includes(emoji)) { 62 | totalUpvotes += numReactions; 63 | } 64 | }); 65 | const timestamp = Date.parse( 66 | comment 67 | .getElementsByClassName("js-timestamp")[0] 68 | .children[0].getAttribute("datetime")! 69 | ); 70 | comments.push({ comment, upvotes: totalUpvotes, timestamp: timestamp }); 71 | }); 72 | 73 | sortComments(comments); 74 | 75 | return comments; 76 | }; 77 | 78 | const sortComments = (comments: CommentData[]) => { 79 | comments 80 | .sort(function (x: CommentData, y: CommentData) { 81 | var n = x.upvotes - y.upvotes; 82 | if (n !== 0) { 83 | return n; 84 | } 85 | 86 | return x.timestamp - y.timestamp; 87 | }) 88 | .reverse(); 89 | }; 90 | -------------------------------------------------------------------------------- /src/mount_view.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import { motion, MotionProps } from "framer-motion"; 3 | 4 | export type AnimationEasing = 5 | | "sSoft" 6 | | "sMedium" 7 | | "sHard" 8 | | "expIn" 9 | | "expOut" 10 | | "expInOut"; 11 | 12 | export const Easing: { [key in AnimationEasing]: number[] } = { 13 | sHard: [0.95, 0, 0.5, 1], 14 | sMedium: [0.8, 0, 0.2, 1], 15 | sSoft: [0.6, 0, 0.4, 1], 16 | expIn: [0.9, 0.05, 1, 0.3], 17 | expOut: [0.05, 0.7, 0.1, 1], 18 | expInOut: [0.9, 0.05, 0.1, 1], 19 | }; 20 | 21 | export interface AnimatedMountViewProps { 22 | styles?: React.CSSProperties; 23 | easing?: AnimationEasing; 24 | duration?: number; 25 | motionProps?: MotionProps; 26 | mountDirection?: "x" | "y" | "none"; 27 | mountInitialOffset?: number; 28 | delay?: number; 29 | className?: string; 30 | } 31 | 32 | const AnimatedMountView: React.FC = ({ 33 | styles, 34 | easing, 35 | duration, 36 | motionProps, 37 | children, 38 | mountDirection, 39 | mountInitialOffset, 40 | delay, 41 | className, 42 | }) => { 43 | const positionFinal = useMemo(() => { 44 | return mountDirection === "x" 45 | ? { x: 0 } 46 | : mountDirection === "none" 47 | ? {} 48 | : { y: 0 }; 49 | }, [mountDirection]); 50 | 51 | const initialOffset = useMemo(() => { 52 | return mountInitialOffset || 16; 53 | }, [mountInitialOffset]); 54 | 55 | const positionInitial = useMemo(() => { 56 | return mountDirection === "x" 57 | ? { x: initialOffset } 58 | : mountDirection === "none" 59 | ? {} 60 | : { y: initialOffset }; 61 | }, [initialOffset, mountDirection]); 62 | 63 | const divProps = useMemo(() => { 64 | return { 65 | animate: { ...positionFinal, opacity: 1 }, 66 | initial: { ...positionInitial, opacity: 0 }, 67 | transition: { 68 | ease: Easing[easing || "expOut"], 69 | duration: duration || 0.4, 70 | delay, 71 | }, 72 | }; 73 | }, [positionFinal, positionInitial, duration, easing, delay]); 74 | 75 | return ( 76 | 82 | {children} 83 | 84 | ); 85 | }; 86 | 87 | const FadeAnimatedMountView: React.FC = (props) => { 88 | return ; 89 | }; 90 | 91 | export default Object.assign(AnimatedMountView, { 92 | Fade: FadeAnimatedMountView, 93 | }); 94 | -------------------------------------------------------------------------------- /src/options.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | const Options = () => { 5 | // const [color, setColor] = useState(""); 6 | // const [status, setStatus] = useState(""); 7 | // const [like, setLike] = useState(false); 8 | 9 | // useEffect(() => { 10 | // // Restores select box and checkbox state using the preferences 11 | // // stored in chrome.storage. 12 | // chrome.storage.sync.get( 13 | // { 14 | // favoriteColor: "red", 15 | // likesColor: true, 16 | // }, 17 | // (items) => { 18 | // setColor(items.favoriteColor); 19 | // setLike(items.likesColor); 20 | // } 21 | // ); 22 | // }, []); 23 | 24 | // const saveOptions = () => { 25 | // // Saves options to chrome.storage.sync. 26 | // chrome.storage.sync.set( 27 | // { 28 | // favoriteColor: color, 29 | // likesColor: like, 30 | // }, 31 | // () => { 32 | // // Update status to let user know options were saved. 33 | // setStatus("Options saved."); 34 | // const id = setTimeout(() => { 35 | // setStatus(""); 36 | // }, 1000); 37 | // return () => clearTimeout(id); 38 | // } 39 | // ); 40 | // }; 41 | 42 | return ( 43 | <> 44 | {/*
45 | Favorite color: 54 |
55 |
56 | 64 |
65 |
{status}
66 | */} 67 | 68 | ); 69 | }; 70 | 71 | ReactDOM.render( 72 | 73 | 74 | , 75 | document.getElementById("root") 76 | ); 77 | -------------------------------------------------------------------------------- /src/popup.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | margin: 0px; 4 | padding: 0px; 5 | width: 100%; 6 | background-color: #0d1117; 7 | border-width: 1.5px; 8 | border-style: solid; 9 | box-sizing: border-box; 10 | border-color: #30363d; 11 | } 12 | 13 | .btn { 14 | background-color: #30a14e; 15 | border: none; 16 | width: 100%; 17 | padding: 5px 16px; 18 | color: #fff; 19 | font-size: 14px; 20 | font-family: "system-ui", "Segoe UI"; 21 | font-weight: 500; 22 | cursor: pointer; 23 | border-radius: 6px; 24 | box-sizing: border-box; 25 | transition-duration: 0.2s; 26 | transition-property: background-color, border-color, opacity; 27 | } 28 | 29 | .spacer { 30 | width: 16px; 31 | } 32 | 33 | .popup { 34 | width: 256px; 35 | border-radius: 6px; 36 | } 37 | 38 | .btn:hover:enabled { 39 | background-color: #40c463; 40 | } 41 | 42 | .btn:active:enabled { 43 | opacity: 0.9; 44 | } 45 | 46 | .btn-dark { 47 | background-color: #0d1117; 48 | border-width: 1.5px; 49 | border-style: solid; 50 | border-color: #30363d; 51 | } 52 | 53 | .btn-dark:hover:enabled { 54 | background-color: #0d1117; 55 | border-color: #8b949e; 56 | } 57 | 58 | .disabled { 59 | opacity: 0.3; 60 | cursor: default; 61 | } 62 | 63 | .btn-dark:active:enabled { 64 | opacity: 0.8; 65 | } 66 | 67 | .buttons { 68 | display: flex; 69 | justify-content: space-between; 70 | padding: 16px; 71 | padding-bottom: 0px; 72 | } 73 | 74 | .no-parse { 75 | padding: 16px; 76 | } 77 | 78 | .header { 79 | display: flex; 80 | align-items: flex-end; 81 | justify-content: space-between; 82 | /* margin-bottom: 16px; */ 83 | /* width: 100%; */ 84 | padding: 12px; 85 | border-bottom-color: #30363d; 86 | border-bottom-style: solid; 87 | border-bottom-width: 1px; 88 | background-color: #161b22; 89 | } 90 | .header-left { 91 | display: flex; 92 | align-items: center; 93 | } 94 | 95 | .icon { 96 | margin-top: 2px; 97 | margin-left: 8px; 98 | width: 14px; 99 | height: 14px; 100 | } 101 | 102 | /* a:hover { 103 | cursor: pointer; 104 | opacity: 0.8; 105 | } */ 106 | 107 | p { 108 | color: #fff; 109 | font-size: 14px; 110 | font-family: "system-ui", "Segoe UI"; 111 | font-weight: 500; 112 | padding: 0px; 113 | margin: 0px; 114 | cursor: default; 115 | } 116 | 117 | .subtext { 118 | color: #8b949e; 119 | font-size: 12px; 120 | } 121 | 122 | .subheader { 123 | color: #8b949e; 124 | } 125 | 126 | .subtext-white { 127 | color: white; 128 | font-size: 12px; 129 | } 130 | 131 | .comment-info-line { 132 | flex-shrink: 1; 133 | width: 2px; 134 | padding-top: 4px; 135 | padding-bottom: 4px; 136 | margin-left: 8px; 137 | margin-right: 8px; 138 | background-color: #30363d; 139 | } 140 | 141 | .comment-info-container { 142 | display: flex; 143 | flex-direction: column; 144 | padding: 12px; 145 | } 146 | 147 | .comment-info-inner { 148 | display: flex; 149 | /* background-color: purple; */ 150 | padding-top: 8px; 151 | } 152 | 153 | .comment-info { 154 | flex: 1; 155 | } 156 | 157 | .highest-voted { 158 | color: #8b949e; 159 | font-size: 12px; 160 | } 161 | 162 | .highest-voted-container { 163 | padding-top: 12px; 164 | padding-bottom: 12px; 165 | } 166 | 167 | .comment-info-item { 168 | display: flex; 169 | justify-content: space-between; 170 | padding-top: 6px; 171 | padding-bottom: 6px; 172 | } 173 | -------------------------------------------------------------------------------- /src/popup.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import AnimatedMount from "./mount_view"; 4 | import "./popup.css"; 5 | import GithubIcon from "../assets/Github.svg"; 6 | interface CommentInfoItemProps { 7 | text?: string; 8 | data?: string; 9 | } 10 | 11 | const CommentInfoItem: React.FC = ({ text, data }) => { 12 | return ( 13 |
14 |

{text}

15 | 21 |

{data}

22 |
23 |
24 | ); 25 | }; 26 | 27 | const Popup = () => { 28 | const [currentURL, setCurrentURL] = useState(); 29 | const [currentComment, setCurrentComment] = useState(); 30 | const [comments, setComments] = useState([]); 31 | const [index, setIndex] = useState(-1); 32 | const [hasNext, setHasNext] = useState(true); 33 | const [hasLast, setHasLast] = useState(false); 34 | const [date, setDate] = useState(""); 35 | 36 | useEffect(() => { 37 | let d = new Date(currentComment?.timestamp!); 38 | setDate( 39 | d.getUTCHours() + 40 | ":" + 41 | (d.getUTCMinutes() < 10 ? "0" : "") + 42 | d.getUTCMinutes() + 43 | ", " + 44 | (d.getMonth() + 1) + 45 | "/" + 46 | d.getDate() + 47 | "/" + 48 | `${d.getFullYear()}`.slice(-2) 49 | ); 50 | }, [currentComment]); 51 | 52 | useEffect(() => { 53 | loadComments(); 54 | }, []); 55 | 56 | useEffect(() => { 57 | sendMessage("scrollTo", { index: index }, (currentComment) => { 58 | setCurrentComment(currentComment); 59 | }); 60 | console.log(index); 61 | if (comments.length > 0) { 62 | if (index + 1 < comments.length) setHasNext(true); 63 | else setHasNext(false); 64 | if (index - 1 >= 0) setHasLast(true); 65 | else setHasLast(false); 66 | } 67 | }, [index]); 68 | 69 | useEffect(() => { 70 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 71 | setCurrentURL(tabs[0].url); 72 | }); 73 | }, []); 74 | 75 | const loadComments = () => { 76 | sendMessage("loadComments", {}, (comments) => { 77 | setComments(comments); 78 | }); 79 | }; 80 | 81 | const sendMessage = ( 82 | type: string, 83 | message: object, 84 | responseCallback?: (res: any) => any 85 | ) => { 86 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 87 | const tab = tabs[0]; 88 | if (tab.id) { 89 | chrome.tabs.sendMessage(tab.id, { ...message, type }, responseCallback); 90 | } 91 | }); 92 | }; 93 | 94 | const nextComment = () => { 95 | if (index + 1 < comments.length) { 96 | setIndex(index + 1); 97 | } 98 | if (index + 1 < comments.length) setHasNext(true); 99 | else setHasNext(false); 100 | }; 101 | 102 | const lastComment = () => { 103 | if (index - 1 >= 0) { 104 | setIndex(index - 1); 105 | } 106 | }; 107 | 108 | return ( 109 |
110 |
111 |
112 |

Skip the Scroll

113 | 117 | 118 | 119 |
120 |

1.0.0

121 |
122 |
123 | {comments && comments.length > 0 ? ( 124 |
125 | 132 |
133 | 140 |
141 | ) : ( 142 |
143 |

Unable to parse the current tab

144 |
145 | )} 146 |
147 | {comments && comments.length > 0 ? ( 148 |
149 |

Comment Information:

150 |
151 |
152 |
153 | {currentComment ? ( 154 | <> 155 | 159 | 160 | {index === 0 ? ( 161 | 162 |
163 |

164 | This is the most recent highest rated response 165 |

166 |
167 |
168 | ) : ( 169 | <> 170 | )} 171 | 172 | ) : ( 173 | 174 | )} 175 |
176 |
177 |
178 | ) : ( 179 | <> 180 | )} 181 |
182 | ); 183 | }; 184 | 185 | ReactDOM.render( 186 | 187 | 188 | , 189 | document.getElementById("root") 190 | ); 191 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: any; 3 | export default content; 4 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "target": "es6", 6 | "esModuleInterop": true, 7 | "sourceMap": false, 8 | "rootDir": "src", 9 | "outDir": "dist/js", 10 | "noEmitOnError": true, 11 | "jsx": "react", 12 | "typeRoots": [ "node_modules/@types" ] 13 | } 14 | } -------------------------------------------------------------------------------- /webpack/webpack.common.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | const CopyPlugin = require("copy-webpack-plugin"); 4 | const srcDir = path.join(__dirname, "..", "src"); 5 | 6 | module.exports = { 7 | entry: { 8 | popup: path.join(srcDir, 'popup.tsx'), 9 | options: path.join(srcDir, 'options.tsx'), 10 | content_script: path.join(srcDir, 'content_script.tsx'), 11 | }, 12 | output: { 13 | path: path.join(__dirname, "../dist/js"), 14 | filename: "[name].js", 15 | }, 16 | optimization: { 17 | splitChunks: { 18 | name: "vendor", 19 | chunks(chunk) { 20 | return chunk.name !== 'background'; 21 | } 22 | }, 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.tsx?$/, 28 | use: "ts-loader", 29 | exclude: /node_modules/, 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: [ 34 | 'style-loader', 35 | 'css-loader' 36 | ] 37 | }, 38 | { 39 | test: /\.svg$/, 40 | use: ['@svgr/webpack'], 41 | }, 42 | ], 43 | }, 44 | resolve: { 45 | extensions: [".ts", ".tsx", ".js"], 46 | }, 47 | plugins: [ 48 | new CopyPlugin({ 49 | patterns: [{ from: ".", to: "../", context: "public" }], 50 | options: {}, 51 | }), 52 | ], 53 | }; 54 | -------------------------------------------------------------------------------- /webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | devtool: 'inline-source-map', 6 | mode: 'development' 7 | }); -------------------------------------------------------------------------------- /webpack/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'production' 6 | }); --------------------------------------------------------------------------------