├── .eslintrc ├── .gitattributes ├── .gitignore ├── .markdownlint.json ├── .prettierrc.json ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── README.zh-CN.md ├── architure ├── diagram.py ├── techstack.png └── web_service.png ├── img ├── 1.png ├── 2.png ├── 3.png ├── chrome.en.png ├── chrome.svg ├── chrome.zh.png ├── edge.en.png ├── edge.svg ├── edge.zh.png ├── firefox.en.png ├── firefox.svg ├── firefox.zh.png ├── image.png ├── install.html └── logo.png ├── package.json ├── privacy ├── src ├── _locales │ └── en │ │ ├── message.json │ │ └── messages.json ├── assets │ ├── icons │ │ ├── icon-128.png │ │ ├── icon-16.png │ │ ├── icon-24.png │ │ ├── icon-32.png │ │ ├── icon-48.png │ │ └── icon-64.png │ └── options │ │ ├── 1.png │ │ ├── 2.png │ │ └── 3.png ├── background │ └── index.ts ├── baseManifest_chrome.json ├── baseManifest_edge.json ├── baseManifest_firefox.json ├── baseManifest_opera.json ├── components │ ├── Category │ │ └── Category.tsx │ ├── Check │ │ └── Check.tsx │ ├── Dependency │ │ └── Dependency.tsx │ ├── Failed │ │ └── Failed.tsx │ ├── Feedback │ │ └── Feedback.tsx │ ├── Loading │ │ └── Loading.tsx │ ├── PrivateRepo │ │ └── PrivateRepo.tsx │ └── Radio │ │ └── Radio.tsx ├── content │ ├── TechStacks.tsx │ ├── content.css │ ├── index.tsx │ ├── isGithubRepoPage.ts │ ├── messageListener.ts │ └── test.js ├── i18n.js ├── options │ ├── Container.tsx │ ├── index.html │ ├── options.css │ └── options.tsx ├── popup │ ├── index.html │ └── index.tsx ├── types │ ├── message.d.ts │ ├── openControl.d.ts │ ├── response.d.ts │ └── techstack.ts └── utils │ ├── get_option.ts │ ├── isGithubRepoPage.ts │ ├── isGithubRepoPage_test.js │ ├── sendMessages.ts │ └── storage.ts ├── tsconfig.json ├── webpack.config.ts ├── webpack.config.utils.ts └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2020, // Allows for the parsing of modern ECMAScript features 5 | "sourceType": "module", // Allows for the use of imports 6 | "ecmaFeatures": { 7 | "jsx": true // Allows for the parsing of JSX 8 | } 9 | }, 10 | "settings": { 11 | "react": { 12 | "version": "detect" // Tells eslint-plugin-react to automatically detect the version of React to use 13 | } 14 | }, 15 | "plugins": ["@typescript-eslint", "prettier"], 16 | "rules": { 17 | "react/prop-types": [1], 18 | "prettier/prettier": [2], 19 | "@typescript-eslint/ban-ts-comment": [0], 20 | "@typescript-eslint/no-explicit-any": [1], 21 | "@typescript-eslint/no-var-requires": [0] 22 | }, 23 | "extends": ["plugin:react/recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"] 24 | } 25 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dev 4 | dist 5 | temp 6 | yarn-error.log -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "no-inline-html": { 3 | "allowed_elements": ["details", "p", "div", "h1", "strong"] 4 | }, 5 | "line-length": false 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": true, 5 | "useTabs": false, 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[markdown]": { 3 | "editor.formatOnSave": false 4 | }, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll": true 7 | }, 8 | "editor.tabSize": 4, 9 | "editor.defaultFormatter": "esbenp.prettier-vscode", 10 | "editor.formatOnSave": true, 11 | "editor.formatOnType": true, 12 | "i18n-ally.localesPaths": [], 13 | "cSpell.words": ["fingerprintjs", "techstack", "webextension"] 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 WebEXP0528 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 |

2 | 3 |

4 | 5 | English | [简体中文](./README.zh-CN.md) 6 | # What is Tech Stack 7 | Tech Stack is a browser extension that will display the tech stack of the GitHub repository you are visiting. 8 | 9 | Discuss in [Discord](https://discord.gg/hEXF9utNHH) / [Telegram](https://t.me/gettechstack) 10 | 11 |

12 | 13 | 14 | 15 |

16 | 17 | Request a new tech stack through [this page](https://submit-techstack.zeabur.app/). It will be added to the System as soon as possible. 18 | We also welcome any developer to submit their own tech stack.😎 19 | 20 | 21 | 22 | ![Alt text](img/1.png) 23 | ![Alt text](img/2.png) 24 | 25 | 26 | Click to Install 27 | 28 | 29 | 30 | Click to Install 31 | 32 | 33 | 34 | Click to Install 35 | 36 | 37 | ## Feedback 38 | If you have any questions or suggestions, please feel free to contact us by opening an [issue on GitHub](https://github.com/Get-Tech-Stack/TechStack/issues). 39 | 40 | ## About TechStack 41 | TechStack will be free forever. But for now, open source is only one part of TechStack. Since the extension is running in users's browsers, we need open source to be transparent and let users know what we are doing in the extension. It enforces the "Don't be evil" policy. 42 | 43 | ## Support ❤️ 44 | You can support the development and maintenance of TechStack with a small contribution through [TechStack's Olostep page](https://donations.olostep.com/tech-stack-github) 45 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | [English](./README.md) | 简体中文 6 | # What is Tech Stack 7 | Tech Stack 是一个浏览器拓展,它将展示你正在访问的 GitHub 仓库的技术栈。 8 | 9 | 社区 [Discord](https://discord.gg/hEXF9utNHH) / [Telegram](https://t.me/gettechstack) 10 | 11 |

12 | 13 | 14 | 15 |

16 | 17 | 请求识别技术栈[点这里](https://submit-techstack.zeabur.app/). 它会尽快被添加到系统中。 18 | 我们也欢迎任何开发者提交自己开发的技术栈。😎 19 | 20 | 21 | 22 | ![Alt text](img/1.png) 23 | ![Alt text](img/2.png) 24 | 25 | 26 | Click to Install 27 | 28 | 29 | 30 | Click to Install 31 | 32 | 33 | 34 | Click to Install 35 | 36 | 37 | 38 | ## Feedback 39 | 如果您有任何问题或建议,请随时通过 GitHub Issue 与我们联系。 40 | 41 | ## About the TechStack 42 | TechStack 将永远免费。 但现在开源只是 TechStack 的一部分。 因为扩展是在用户的浏览器中运行的,所以我们需要开源来让用户知道我们在扩展中做了什么。 它可以确保不作恶。 -------------------------------------------------------------------------------- /architure/diagram.py: -------------------------------------------------------------------------------- 1 | from diagrams import Diagram 2 | from diagrams.aws.compute import EC2 3 | from diagrams.aws.database import RDSPostgresqlInstance,DocumentdbMongodbCompatibility 4 | from diagrams.aws.network import ELB 5 | from diagrams.programming.language import Go,Javascript 6 | from diagrams.programming.framework import React 7 | 8 | 9 | 10 | 11 | with Diagram("TechStack", show=False): 12 | techstack_backend = Go("TechStack Backend") 13 | techstack_center = Go("TechStack Data") 14 | techstack_editor = React("TechStack Editor") 15 | techstack = Javascript("TechStack") 16 | 17 | techstack_editor >> techstack_center 18 | techstack >> techstack_backend 19 | techstack_backend >> techstack_center >> RDSPostgresqlInstance("tehcstack data") 20 | techstack_backend >> DocumentdbMongodbCompatibility("GitHub Repo ") 21 | -------------------------------------------------------------------------------- /architure/techstack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/architure/techstack.png -------------------------------------------------------------------------------- /architure/web_service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/architure/web_service.png -------------------------------------------------------------------------------- /img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/img/1.png -------------------------------------------------------------------------------- /img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/img/2.png -------------------------------------------------------------------------------- /img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/img/3.png -------------------------------------------------------------------------------- /img/chrome.en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/img/chrome.en.png -------------------------------------------------------------------------------- /img/chrome.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /img/chrome.zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/img/chrome.zh.png -------------------------------------------------------------------------------- /img/edge.en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/img/edge.en.png -------------------------------------------------------------------------------- /img/edge.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /img/edge.zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/img/edge.zh.png -------------------------------------------------------------------------------- /img/firefox.en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/img/firefox.en.png -------------------------------------------------------------------------------- /img/firefox.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /img/firefox.zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/img/firefox.zh.png -------------------------------------------------------------------------------- /img/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/img/image.png -------------------------------------------------------------------------------- /img/install.html: -------------------------------------------------------------------------------- 1 | 44 | 45 | 52 | 53 | 54 | 55 |
56 |
Applies to Mozilla Firefox
57 |
Install from Firefox store
58 |
59 |
60 | 61 | 62 | 63 |
64 |
适用于 to Edge
65 |
从 Edge 商店安装
66 |
67 |
68 | 69 | 70 | 71 |
72 |
Applies to Edge
73 |
Install from Edge store
74 |
75 |
76 | 77 | 78 | 79 |
80 |
Applies to Chrome
81 |
Install from Chrome store
82 |
83 |
84 | 85 | 86 | 87 |
88 |
适用于 to Chrome
89 |
从 Chrome 商店安装
90 |
91 |
92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/img/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tech-stack", 3 | "version": "0.1.2", 4 | "description": "", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "dev:chrome": "cross-env NODE_ENV=development TARGET=chrome webpack", 8 | "dev:firefox": "cross-env NODE_ENV=development TARGET=firefox webpack", 9 | "dev:opera": "cross-env NODE_ENV=development TARGET=opera webpack", 10 | "dev:edge": "cross-env NODE_ENV=development TARGET=edge webpack", 11 | "profile:chrome": "cross-env NODE_ENV=profile TARGET=chrome webpack", 12 | "profile:firefox": "cross-env NODE_ENV=profile TARGET=firefox webpack", 13 | "profile:opera": "cross-env NODE_ENV=profile TARGET=opera webpack", 14 | "profile:edge": "cross-env NODE_ENV=profile TARGET=edge webpack", 15 | "build:chrome": "cross-env NODE_ENV=production TARGET=chrome webpack", 16 | "build:firefox": "cross-env NODE_ENV=production TARGET=firefox webpack", 17 | "build:opera": "cross-env NODE_ENV=production TARGET=opera webpack", 18 | "build:edge": "cross-env NODE_ENV=production TARGET=edge webpack", 19 | "upload:chrome": "cross-env NODE_ENV=upload TARGET=chrome webpack", 20 | "upload:firefox": "cross-env NODE_ENV=upload TARGET=firefox webpack", 21 | "upload:opera": "cross-env NODE_ENV=upload TARGET=opera webpack", 22 | "upload:edge": "cross-env NODE_ENV=upload TARGET=edge webpack", 23 | "build": "yarn run build:chrome && yarn run build:firefox && yarn run build:opera && yarn run build:edge", 24 | "upload": "yarn run upload:chrome && yarn run upload:firefox && yarn run upload:opera && yarn run upload:edge", 25 | "lint-fix": "eslint --ext js,jsx,ts,tsx, src --fix" 26 | }, 27 | "author": "Web Dev", 28 | "dependencies": { 29 | "@fingerprintjs/fingerprintjs": "v3.3.4", 30 | "@radix-ui/colors": "^2.1.0", 31 | "@radix-ui/react-icons": "^1.3.0", 32 | "@radix-ui/react-radio-group": "^1.1.3", 33 | "@radix-ui/react-switch": "^1.0.3", 34 | "@radix-ui/react-toggle-group": "^1.0.4", 35 | "clean-webpack-plugin": "^4.0.0", 36 | "copy-webpack-plugin": "^11.0.0", 37 | "core-js": "^3.32.0", 38 | "cross-env": "^7.0.3", 39 | "css-loader": "^6.8.1", 40 | "dotenv": "^16.3.1", 41 | "html-webpack-plugin": "^5.5.3", 42 | "i18next": "^23.4.4", 43 | "i18next-browser-languagedetector": "^7.1.0", 44 | "i18next-http-backend": "^2.2.1", 45 | "prop-types": "^15.8.1", 46 | "react": "^18.2.0", 47 | "react-animate-height": "^3.2.2", 48 | "react-dom": "^18.2.0", 49 | "react-i18next": "^13.1.1", 50 | "react-use-firework": "^1.0.2", 51 | "style-loader": "^3.3.3", 52 | "swr": "^2.2.0", 53 | "terser-webpack-plugin": "^5.3.9", 54 | "ts-node": "^10.9.1", 55 | "webextension-polyfill": "^0.10.0", 56 | "webpack": "^5.88.2", 57 | "webpack-bundle-analyzer": "^4.9.0", 58 | "webpack-ext-reloader-mv3": "^2.1.1", 59 | "webpack-extension-manifest-plugin": "^0.8.0", 60 | "zip-webpack-plugin": "^4.0.1" 61 | }, 62 | "devDependencies": { 63 | "@types/chrome": "^0.0.242", 64 | "@types/inboxsdk": "^2.0.10", 65 | "@types/jquery": "^3.5.16", 66 | "@types/lodash": "^4.14.196", 67 | "@types/node": "^20.4.5", 68 | "@types/react": "^18.2.18", 69 | "@types/react-dom": "^18.2.7", 70 | "@types/react-router-dom": "^5.3.3", 71 | "@types/redux": "^3.6.31", 72 | "@types/webpack-bundle-analyzer": "^4.6.0", 73 | "@types/zip-webpack-plugin": "^3.0.3", 74 | "@typescript-eslint/eslint-plugin": "^6.2.1", 75 | "@typescript-eslint/parser": "^6.2.1", 76 | "eslint": "^8.46.0", 77 | "eslint-config-airbnb": "^19.0.4", 78 | "eslint-config-prettier": "^8.9.0", 79 | "eslint-plugin-babel": "^5.3.1", 80 | "eslint-plugin-import": "^2.28.0", 81 | "eslint-plugin-jsx-a11y": "^6.7.1", 82 | "eslint-plugin-prettier": "^5.0.0", 83 | "eslint-plugin-react": "^7.33.1", 84 | "eslint-plugin-react-hooks": "^4.6.0", 85 | "eslint-webpack-plugin": "^4.0.1", 86 | "prettier": "^3.0.0", 87 | "ts-loader": "^9.4.4", 88 | "typescript": "^5.1.6", 89 | "webpack-cli": "^5.1.4" 90 | }, 91 | "license": "SEE LICENSE IN LICENSE", 92 | "repository": { 93 | "type": "git", 94 | "url": "https://github.com/WebExp0528/React-Extension-Boilerplate" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /privacy: -------------------------------------------------------------------------------- 1 | Privacy Policy 2 | Introduction 3 | Welcome to TechStack Corporation(“us”, “we”, or “our”) operates https://github.com/Get-Tech-Stack/TechStack(opens in a new tab) (hereinafter referred to as “Service”). 4 | 5 | Our Privacy Policy governs your visit to https://github.com/Get-Tech-Stack/TechStack(opens in a new tab), and explains how we collect, safeguard and disclose information that results from your use of our Service. We use your data to provide and improve Service. 6 | 7 | By using Service, you agree to the collection and use of information in accordance with this policy. 8 | 9 | Unless otherwise defined in this Privacy Policy, the terms used in this Privacy Policy have the same meanings as in our Terms and Conditions. 10 | 11 | Our Terms and Conditions (“Terms”) govern all use of our Service and together with the Privacy Policy constitutes your agreement with us (“agreement”). 12 | 13 | Definitions 14 | SERVICE means the https://github.com/Get-Tech-Stack/TechStack(opens in a new tab) website operated by TechStack Corporation. 15 | 16 | PERSONAL DATA means data about a living individual who can be identified from those data (or from those and other information either in our possession or likely to come into our possession). 17 | 18 | USAGE DATA is data collected automatically either generated by the use of Service or from Service infrastructure itself (for example, the duration of a page visit). 19 | 20 | COOKIES are small files stored on your device (computer or mobile device). 21 | 22 | DATA CONTROLLER means a natural or legal person who (either alone or jointly or in common with other persons) determines the purposes for which and the manner in which any personal data are, or are to be, processed. For the purpose of this Privacy Policy, we are a Data Controller of your data. 23 | 24 | DATA PROCESSORS (OR SERVICE PROVIDERS) means any natural or legal person who processes the data on behalf of the Data Controller. We may use the services of various Service Providers in order to process your data more effectively. 25 | 26 | DATA SUBJECT is any living individual who is the subject of Personal Data. 27 | 28 | THE USER is the individual using our Service. The User corresponds to the Data Subject, who is the subject of Personal Data. 29 | 30 | Information Collection and Use 31 | We collect several different types of information for various purposes to provide and improve our Service to you. 32 | 33 | Types of Data Collected 34 | Personal Data 35 | While using our Service, we may ask you to provide us with certain personally identifiable information that can be used to contact or identify you (“Personal Data”). Personally identifiable information may include, but is not limited to: 36 | 37 | Email address 38 | First name and last name 39 | Cookies and usage data 40 | We may use your Personal Data to contact you with newsletters, marketing or promotional materials and other information that may be of interest to you. You may opt out of receiving any, or all, of these communications from us by following the unsubscribe link. 41 | 42 | Usage Data 43 | We may also collect information that your browser sends whenever you visit our Service or when you access Service by or through a mobile device (“Usage Data”). 44 | 45 | This Usage Data may include information such as your computer's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that you visit, the time and date of your visit, the time spent on those pages, unique device identifiers and other diagnostic data. 46 | 47 | When you access Service with a mobile device, this Usage Data may include information such as the type of mobile device you use, your mobile device unique ID, the IP address of your mobile device, your mobile operating system, the type of mobile Internet browser you use, unique device identifiers and other diagnostic data. 48 | 49 | Tracking Cookies Data 50 | We use cookies and similar tracking technologies to track the activity on our Service and we hold certain information. 51 | 52 | Cookies are files with a small amount of data which may include an anonymous unique identifier. Cookies are sent to your browser from a website and stored on your device. Other tracking technologies are also used such as beacons, tags and scripts to collect and track information and to improve and analyze our Service. 53 | 54 | You can instruct your browser to refuse all cookies or to indicate when a cookie is being sent. However, if you do not accept cookies, you may not be able to use some portions of our Service. 55 | 56 | Examples of Cookies we use: 57 | 58 | Session Cookies: We use Session Cookies to operate our Service. 59 | Preference Cookies: We use Preference Cookies to remember your preferences and various settings. 60 | Security Cookies: We use Security Cookies for security purposes. 61 | Other Data 62 | While using our Service, we may also collect the following information: sex, age, date of birth, place of birth, passport details, citizenship, registration at place of residence and actual address, telephone number (work, mobile), details of documents on education, qualification, professional training, employment agreements, non-disclosure agreements, information on bonuses and compensation, information on marital status, family members, social security (or other taxpayer identification) number, office location and other data. 63 | 64 | Use of Data 65 | TechStack Corporation uses the collected data for various purposes: 66 | 67 | to provide and maintain our Service; 68 | to notify you about changes to our Service; 69 | to allow you to participate in interactive features of our Service when you choose to do so; 70 | to provide customer support; 71 | to gather analysis or valuable information so that we can improve our Service; 72 | to monitor the usage of our Service; 73 | to detect, prevent and address technical issues; 74 | to fulfill any other purpose for which you provide it; 75 | to carry out our obligations and enforce our rights arising from any contracts entered into between you and us, including for billing and collection; 76 | to provide you with notices about your account and/or subscription, including expiration and renewal notices, email-instructions, etc.; 77 | to provide you with news, special offers and general information about other goods, services and events which we offer that are similar to those that you have already purchased or enquired about unless you have opted not to receive such information; 78 | in any other way we may describe when you provide the information; 79 | for any other purpose with your consent. 80 | Retention of Data 81 | We will retain your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies. 82 | 83 | We will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period, except when this data is used to strengthen the security or to improve the functionality of our Service, or we are legally obligated to retain this data for longer time periods. 84 | 85 | Disclosure Of Data 86 | We may disclose personal information that we collect, or you provide: 87 | 88 | Disclosure for Law Enforcement. Under certain circumstances, we may be required to disclose your Personal Data if required to do so by law or in response to valid requests by public authorities. 89 | 90 | Business Transaction. If we or our subsidiaries are involved in a merger, acquisition or asset sale, your Personal Data may be transferred. 91 | 92 | Other cases. We may disclose your information also: 93 | 94 | to our subsidiaries and affiliates; 95 | 96 | to fulfill the purpose for which you provide it; 97 | 98 | for the purpose of including your company’s logo on our website; 99 | 100 | if we believe disclosure is necessary or appropriate to protect the rights, property, or safety of the Company, our customers, or others. 101 | 102 | Security of Data 103 | The security of your data is important to us but remember that no method of transmission over the Internet or method of electronic storage is 100% secure. While we strive to use commercially acceptable means to protect your Personal Data, we cannot guarantee its absolute security. 104 | 105 | GDPR 106 | If you are a resident of the European Union (EU) and European Economic Area (EEA), you have certain data protection rights, covered by GDPR. – See more at https://eur-lex.europa.eu/eli/reg/2016/679/oj(opens in a new tab) 107 | 108 | We aim to take reasonable steps to allow you to correct, amend, delete, or limit the use of your Personal Data. 109 | 110 | If you wish to be informed what Personal Data we hold about you and if you want it to be removed from our systems, please email us at support@TechStack.com. 111 | 112 | In certain circumstances, you have the following data protection rights: 113 | 114 | the right to access, update or to delete the information we have on you; 115 | 116 | the right of rectification. You have the right to have your information rectified if that information is inaccurate or incomplete; 117 | 118 | the right to object. You have the right to object to our processing of your Personal Data; 119 | 120 | the right of restriction. You have the right to request that we restrict the processing of your personal information; 121 | 122 | the right to data portability. You have the right to be provided with a copy of your Personal Data in a structured, machine-readable and commonly used format; 123 | 124 | the right to withdraw consent. You also have the right to withdraw your consent at any time where we rely on your consent to process your personal information; 125 | 126 | Please note that we may ask you to verify your identity before responding to such requests. Please note, we may not able to provide Service without some necessary data. 127 | 128 | Service Providers 129 | We may employ third party companies and individuals to facilitate our Service (“Service Providers”), provide Service on our behalf, perform Service-related services or assist us in analysing how our Service is used. 130 | 131 | These third parties have access to your Personal Data only to perform these tasks on our behalf and are obligated not to disclose or use it for any other purpose. 132 | 133 | Analytics 134 | We may use third-party Service Providers to monitor and analyze the use of our Service. 135 | 136 | Google Analytics 137 | 138 | CI/CD tools 139 | We may use third-party Service Providers to automate the development process of our Service. 140 | 141 | GitHub 142 | GitHub is provided by GitHub, Inc. 143 | 144 | GitHub is a development platform to host and review code, manage projects, and build software. 145 | 146 | For more information on what data GitHub collects for what purpose and how the protection of the data is ensured, please visit GitHub Privacy Policy page: 147 | 148 | Behavioral Remarketing 149 | TechStack Corporation uses remarketing services to advertise on third party websites to you after you visited our Service. We and our third-party vendors use cookies to inform, optimise and serve ads based on your past visits to our Service. 150 | 151 | Google Ads (AdWords) 152 | Google Ads (AdWords) remarketing service is provided by Google Inc. 153 | 154 | You can opt-out of Google Analytics for Display Advertising and customize the Google Display Network ads by visiting the Google Ads Settings page: http://www.google.com/settings/ads 155 | 156 | Google also recommends installing the Google Analytics Opt-out Browser Add-on – https://tools.google.com/dlpage/gaoptout(opens in a new tab) – for your web browser. Google Analytics Opt-out Browser Add-on provides visitors with the ability to prevent their data from being collected and used by Google Analytics. 157 | 158 | For more information on the privacy practices of Google, please visit the Google Privacy Terms web page: https://policies.google.com/privacy?hl=en(opens in a new tab) 159 | 160 | Twitter 161 | Twitter remarketing service is provided by Twitter Inc. 162 | 163 | You can opt-out from Twitter's interest-based ads by following their instructions: https://support.twitter.com/articles/20170405(opens in a new tab) 164 | 165 | You can learn more about the privacy practices and policies of Twitter by visiting their Privacy Policy page: https://twitter.com/privacy(opens in a new tab) 166 | 167 | Payments 168 | We may provide paid products and/or services within Service. In that case, we use third-party services 169 | 170 | We will not store or collect your payment card details. That information is provided directly to our third-party payment processors whose use of your personal information is governed by their Privacy Policy. These payment processors adhere to the standards set by PCI-DSS as managed by the PCI Security Standards Council, which is a joint effort of brands like Visa, Mastercard, American Express and Discover. PCI-DSS requirements help ensure the secure handling of payment information. 171 | 172 | The payment processor we work with is Stripe(opens in a new tab). You can view their Privacy Policy here: https://stripe.com/privacy(opens in a new tab) 173 | 174 | Links to Other Sites 175 | Our Service may contain links to other sites that are not operated by us. If you click a third party link, you will be directed to that third party's site. We strongly advise you to review the Privacy Policy of every site you visit. 176 | 177 | We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services. 178 | 179 | Children's Privacy 180 | Our Services are not intended for use by children under the age of 13 (“Children”). 181 | 182 | We do not knowingly collect personally identifiable information from Children under 13. If you become aware that a Child has provided us with Personal Data, please contact us. If we become aware that we have collected Personal Data from Children without verification of parental consent, we take steps to remove that information from our servers. 183 | 184 | Changes to This Privacy Policy 185 | We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page. 186 | 187 | We will let you know via email and/or a prominent notice on our Service, prior to the change becoming effective and update “effective date” at the top of this Privacy Policy. 188 | 189 | You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page. 190 | 191 | Contact Us 192 | If you have any questions about this Privacy Policy, please contact us: 193 | 194 | By email: a778917369@gmail.com -------------------------------------------------------------------------------- /src/_locales/en/message.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Tech Stack: Show Github Repo Tech Stack", 4 | "description": "The name of the react extension." 5 | }, 6 | "appDescription": { 7 | "message": "When a user visits a Github public repository, the extension will display that repository's tech stack alongside the page.", 8 | "description": "The description of the extension." 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Tech Stack: Show Github Repo Tech Stack", 4 | "description": "The name of the react extension." 5 | }, 6 | "appDescription": { 7 | "message": "When a user visits a Github public repository, the extension will display that repository's tech stack alongside the page.", 8 | "description": "The description of the extension." 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/icons/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/src/assets/icons/icon-128.png -------------------------------------------------------------------------------- /src/assets/icons/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/src/assets/icons/icon-16.png -------------------------------------------------------------------------------- /src/assets/icons/icon-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/src/assets/icons/icon-24.png -------------------------------------------------------------------------------- /src/assets/icons/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/src/assets/icons/icon-32.png -------------------------------------------------------------------------------- /src/assets/icons/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/src/assets/icons/icon-48.png -------------------------------------------------------------------------------- /src/assets/icons/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/src/assets/icons/icon-64.png -------------------------------------------------------------------------------- /src/assets/options/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/src/assets/options/1.png -------------------------------------------------------------------------------- /src/assets/options/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/src/assets/options/2.png -------------------------------------------------------------------------------- /src/assets/options/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Get-Tech-Stack/TechStack/d1698f0ca5751e7760874e5fa1895ae623889b38/src/assets/options/3.png -------------------------------------------------------------------------------- /src/background/index.ts: -------------------------------------------------------------------------------- 1 | import { runtime, tabs, Tabs, Runtime } from "webextension-polyfill"; 2 | /** 3 | * Define background script functions 4 | * @type {class} 5 | */ 6 | class Background { 7 | _port: number; 8 | constructor() { 9 | this.init(); 10 | } 11 | 12 | /** 13 | * Document Ready 14 | * 15 | * @returns {void} 16 | */ 17 | init = () => { 18 | console.log("[===== Loaded Background Scripts =====]"); 19 | 20 | //When extension installed 21 | runtime.onInstalled.addListener(this.onInstalled); 22 | 23 | //Add message listener in Browser. 24 | runtime.onMessage.addListener(this.onMessage); 25 | 26 | //Add Update listener for tab 27 | tabs.onUpdated.addListener(this.onUpdatedTab); 28 | 29 | //Add New tab create listener 30 | tabs.onCreated.addListener(this.onCreatedTab); 31 | }; 32 | 33 | //TODO: Listeners 34 | 35 | /** 36 | * Extension Installed 37 | */ 38 | onInstalled = () => { 39 | console.log("[===== Installed Extension!] ====="); 40 | }; 41 | 42 | /** 43 | * Message Handler Function 44 | * 45 | * @param message 46 | * @param sender 47 | * @returns 48 | */ 49 | onMessage = async (message: EXTMessage, sender: Runtime.MessageSender) => { 50 | try { 51 | console.log("[===== Received message =====]", message, sender); 52 | switch (message.type) { 53 | } 54 | return true; // result to reply 55 | } catch (error) { 56 | console.log("[===== Error in MessageListener =====]", error); 57 | return error; 58 | } 59 | }; 60 | 61 | /** 62 | * Message from Long Live Connection 63 | * 64 | * @param msg 65 | */ 66 | onMessageFromExtension = (msg: EXTMessage) => { 67 | console.log("[===== Message from Long Live Connection =====]"); 68 | console.log(msg); 69 | }; 70 | 71 | /** 72 | * 73 | * @param tab 74 | */ 75 | onCreatedTab = (tab: Tabs.Tab) => { 76 | console.log("[===== New Tab Created =====]", tab); 77 | }; 78 | 79 | /** 80 | * When changes tabs 81 | * 82 | * @param {*} tabId 83 | * @param {*} changeInfo 84 | * @param {*} tab 85 | */ 86 | onUpdatedTab = ( 87 | tabId: number, 88 | changeInfo: Tabs.OnUpdatedChangeInfoType, 89 | tab: Tabs.Tab 90 | ) => { 91 | console.log("[===== Tab Created =====]", tabId); 92 | if (changeInfo.status === "complete") { 93 | this.sendMessage(tab, { type: "REJECT", data: {} }); 94 | } 95 | }; 96 | 97 | /** 98 | * Get url from tabId 99 | * 100 | */ 101 | getURLFromTab = async (tabId: number) => { 102 | try { 103 | const tab = await tabs.get(tabId); 104 | return tab.url || ""; 105 | } catch (error) { 106 | console.log( 107 | `[===== Could not get Tab Info$(tabId) in getURLFromTab =====]`, 108 | error 109 | ); 110 | throw ""; 111 | } 112 | }; 113 | 114 | /** 115 | * Open new tab by url 116 | * 117 | */ 118 | openNewTab = async (url: string) => { 119 | try { 120 | const tab = await tabs.create({ url }); 121 | return tab; 122 | } catch (error) { 123 | console.log(`[===== Error in openNewTab =====]`, error); 124 | return null; 125 | } 126 | }; 127 | 128 | /** 129 | * Close specific tab 130 | * 131 | * @param {number} tab 132 | */ 133 | closeTab = async (tab: Tabs.Tab) => { 134 | try { 135 | await tabs.remove(tab.id ?? 0); 136 | } catch (error) { 137 | console.log(`[===== Error in closeTab =====]`, error); 138 | } 139 | }; 140 | 141 | /** 142 | * send message 143 | */ 144 | sendMessage = async (tab: Tabs.Tab, msg: EXTMessage) => { 145 | try { 146 | const res = await tabs.sendMessage(tab.id ?? 0, msg); 147 | return res; 148 | } catch (error) { 149 | console.log(`[===== Error in sendMessage =====]`, error); 150 | return null; 151 | } 152 | }; 153 | } 154 | 155 | export const background = new Background(); 156 | -------------------------------------------------------------------------------- /src/baseManifest_chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TechStack: Show Github Repo Tech Stack", 3 | "author": "CorrectRoadH", 4 | "version": "1.25", 5 | "manifest_version": 3, 6 | "description": "The extension will display the tech stack of the Repo when the user visits a GitHub Public Repo.", 7 | "icons": { 8 | "16": "assets/icons/icon-16.png", 9 | "24": "assets/icons/icon-24.png", 10 | "64": "assets/icons/icon-64.png", 11 | "128": "assets/icons/icon-128.png" 12 | }, 13 | "default_locale": "en", 14 | "content_scripts": [ 15 | { 16 | "matches": ["*://github.com/*"], 17 | "js": ["content/content.js"], 18 | "css": ["content/content.css"], 19 | "all_frames": true, 20 | "run_at": "document_start" 21 | } 22 | ], 23 | "background": { 24 | "service_worker": "background/background.js" 25 | }, 26 | "permissions": ["tabs", "storage"], 27 | "options_ui": { 28 | "page": "options/index.html", 29 | "open_in_tab": true 30 | }, 31 | "action": { 32 | "default_icon": { 33 | "16": "assets/icons/icon-16.png", 34 | "48": "assets/icons/icon-48.png" 35 | }, 36 | "default_popup": "popup/index.html", 37 | "default_title": "Teach Stack" 38 | }, 39 | "web_accessible_resources": [ 40 | { 41 | "resources": ["assets/*", "content/*", "options/*", "popup/*", "background/*"], 42 | "matches": [""] 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /src/baseManifest_edge.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TechStack: Display Github Repo Tech Stack", 3 | "author": "CorrectRoadH", 4 | "version": "1.25", 5 | "manifest_version": 3, 6 | "description": "The extension will display the tech stack of the Repo when the user visits a GitHub Public Repo.", 7 | "short description": "Display a Github repo's tech stack", 8 | "icons": { 9 | "16": "assets/icons/icon-16.png", 10 | "24": "assets/icons/icon-24.png", 11 | "64": "assets/icons/icon-64.png", 12 | "128": "assets/icons/icon-128.png" 13 | }, 14 | "default_locale": "en", 15 | "content_scripts": [ 16 | { 17 | "matches": ["*://github.com/*"], 18 | "js": ["content/content.js"], 19 | "css": ["content/content.css"], 20 | "all_frames": true, 21 | "run_at": "document_start" 22 | } 23 | ], 24 | "background": { 25 | "service_worker": "background/background.js" 26 | }, 27 | "permissions": ["tabs", "storage"], 28 | "options_ui": { 29 | "page": "options/index.html", 30 | "open_in_tab": true 31 | }, 32 | "action": { 33 | "default_icon": { 34 | "16": "assets/icons/icon-16.png", 35 | "48": "assets/icons/icon-48.png" 36 | }, 37 | "default_title": "Teach Stack", 38 | "default_popup": "popup/index.html" 39 | }, 40 | "web_accessible_resources": [ 41 | { 42 | "resources": ["assets/*", "content/*", "options/*", "popup/*", "background/*"], 43 | "matches": [""] 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /src/baseManifest_firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TechStack: Show Github Repo Tech Stack", 3 | "author": "CorrectRoadH", 4 | "version": "1.25", 5 | "manifest_version": 2, 6 | "description": "The extension will display the tech stack of the Repo when the user visits a GitHub Public Repo. The user can easily get more info about the repo.", 7 | "icons": { 8 | "16": "assets/icons/icon-16.png", 9 | "24": "assets/icons/icon-24.png", 10 | "64": "assets/icons/icon-64.png", 11 | "128": "assets/icons/icon-128.png" 12 | }, 13 | "default_locale": "en", 14 | "options_ui": { 15 | "page": "options/index.html", 16 | "open_in_tab": true 17 | }, 18 | "content_scripts": [ 19 | { 20 | "matches": ["*://github.com/*"], 21 | "js": ["content/content.js"], 22 | "css": ["content/content.css"], 23 | "all_frames": true, 24 | "run_at": "document_start" 25 | } 26 | ], 27 | "background": { 28 | "scripts": ["background/background.js"] 29 | }, 30 | "permissions": ["*://*.github.com/*", "*://*.zeabur.app/", "activeTab", "webRequest", "tabs", "storage"], 31 | "browser_specific_settings": { 32 | "gecko": { 33 | "id": "a778917369@gmail.com", 34 | "strict_min_version": "54.0a1" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/baseManifest_opera.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_appName__", 3 | "author": "WebDEV", 4 | "version": "1.0", 5 | "manifest_version": 3, 6 | "description": "__MSG_appDescription__", 7 | "icons": { 8 | "16": "assets/icons/icon-16.png", 9 | "24": "assets/icons/icon-24.png", 10 | "64": "assets/icons/icon-64.png", 11 | "128": "assets/icons/icon-128.png" 12 | }, 13 | "default_locale": "en", 14 | "content_scripts": [ 15 | { 16 | "matches": ["http://*/*", "https://*/*"], 17 | "js": ["content/content.js"] 18 | } 19 | ], 20 | "background": { 21 | "service_worker": "background/background.js" 22 | }, 23 | "host_permissions": ["http://*/*", "https://*/*"], 24 | "web_accessible_resources": [ 25 | { 26 | "resources": ["assets/*", "content/*", "options/*", "popup/*", "background/*"], 27 | "matches": [""] 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Category/Category.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import Dependency from '../Dependency/Dependency'; 4 | interface CategoryProps { 5 | name: string; 6 | deps: any; 7 | } 8 | const Category = ({ name, deps }: CategoryProps) => { 9 | const { t } = useTranslation(); 10 | return ( 11 |
12 |
{t(name)}
13 |
14 | {deps.map((dependency: any) => ( 15 | 16 | ))} 17 |
18 |
19 | ); 20 | }; 21 | 22 | export default Category; 23 | -------------------------------------------------------------------------------- /src/components/Check/Check.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import * as Switch from '@radix-ui/react-switch'; 3 | import storage from '../../utils/storage'; 4 | 5 | interface CheckProps { 6 | title: string; 7 | subTitle: string; 8 | 9 | storageKey: string; 10 | defaultValue: boolean; 11 | } 12 | const Check = ({ title, subTitle, storageKey: storageKey, defaultValue }: CheckProps) => { 13 | const [value, setValue] = React.useState(false); 14 | useEffect(() => { 15 | storage.get([storageKey]).then((result) => { 16 | if (result[storageKey] === undefined) { 17 | storage.set({ [storageKey]: defaultValue }); 18 | setValue(defaultValue); 19 | } else { 20 | setValue(result[storageKey]); 21 | } 22 | }); 23 | }, []); 24 | 25 | const handleCheckboxClick = () => { 26 | setValue(!value); 27 | storage.set({ [storageKey]: !value }); 28 | }; 29 | 30 | return ( 31 |
32 |
33 | 34 |
35 | 36 | 37 | 38 |
39 | 42 |
43 |
44 |
45 |
46 | ); 47 | }; 48 | 49 | export default Check; 50 | -------------------------------------------------------------------------------- /src/components/Dependency/Dependency.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface DependencyProps { 4 | data: any; 5 | } 6 | const Dependency = ({ data }: DependencyProps) => { 7 | return ( 8 |
9 | 10 | {data.name} 11 |
{data.name}
12 |
13 |
14 | ); 15 | }; 16 | 17 | export default Dependency; 18 | -------------------------------------------------------------------------------- /src/components/Failed/Failed.tsx: -------------------------------------------------------------------------------- 1 | import Feedback from 'components/Feedback/Feedback'; 2 | import React from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | interface FailedProps { 6 | url: string; 7 | } 8 | const Failed = ({ url }: FailedProps) => { 9 | const { t } = useTranslation(); 10 | 11 | return ( 12 |
13 |
{t('failed-prompt')}
14 | 15 |
16 | ); 17 | }; 18 | 19 | export default Failed; 20 | -------------------------------------------------------------------------------- /src/components/Feedback/Feedback.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import useFireWork from 'react-use-firework'; 4 | import storage from '../../utils/storage'; 5 | 6 | interface PrivateProps { 7 | url: string; 8 | } 9 | 10 | const like = async (url: string) => { 11 | fetch('https://techstack.zeabur.app/like?url=' + url, { 12 | method: 'GET', 13 | credentials: 'include', 14 | }).catch((err) => { 15 | console.log(err); 16 | }); 17 | }; 18 | 19 | const unlike = async (url: string) => { 20 | fetch('https://techstack.zeabur.app/unlike?url=' + url, { 21 | method: 'GET', 22 | credentials: 'include', 23 | }).catch((err) => { 24 | console.log(err); 25 | }); 26 | }; 27 | 28 | const Feedback = ({ url }: PrivateProps) => { 29 | const likeRef = useRef(null); 30 | const unlikeRef = useRef(null); 31 | 32 | const [enableFeedback, setEnableFeedback] = React.useState(true); 33 | 34 | useFireWork(likeRef); 35 | useFireWork(unlikeRef); 36 | 37 | const [liked, setLiked] = React.useState(false); 38 | 39 | const { t } = useTranslation(); 40 | 41 | const handleLikeBtnClick = async (url: string) => { 42 | like(url); 43 | setLiked(true); 44 | }; 45 | 46 | const handleUnlikeBtnClick = async (url: string) => { 47 | unlike(url); 48 | setLiked(true); 49 | }; 50 | 51 | const mailUrl = 52 | 'mailto:a778917369@gmail.com?subject=TechStack%20feedback%3A%20%5B' + 53 | url + 54 | "%5D&body=What's%20the%20problem%3F%0D%0A%0D%0AAny%20suggestions%3F"; 55 | 56 | storage.get(['enable_feedback']).then((result: any) => { 57 | setEnableFeedback(result['enable_feedback'] || result['enable_feedback'] === undefined); 58 | }); 59 | 60 | if (enableFeedback) { 61 | return ( 62 |
63 |
64 | {!liked ? ( 65 |
66 |
{t('feedback-prompt')}
67 |
handleLikeBtnClick(url)}> 68 | 👍 69 |
70 |
handleUnlikeBtnClick(url)}> 71 | 👎 72 |
73 |
74 | ) : ( 75 |
{t('feedback-thank')}
76 | )} 77 |
78 | 79 | feedback 80 | 81 |
82 | ); 83 | } else { 84 | return
; 85 | } 86 | }; 87 | export default Feedback; 88 | -------------------------------------------------------------------------------- /src/components/Loading/Loading.tsx: -------------------------------------------------------------------------------- 1 | import Feedback from 'components/Feedback/Feedback'; 2 | import React from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | interface LoadingProps { 6 | url: string; 7 | } 8 | const Loading = ({ url }: LoadingProps) => { 9 | const { t } = useTranslation(); 10 | 11 | return ( 12 |
13 |
14 | 15 | Loading your activity... 21 | 22 |
{t('loading-prompt')}
23 |
24 | 25 | 26 |
27 | ); 28 | }; 29 | 30 | export default Loading; 31 | -------------------------------------------------------------------------------- /src/components/PrivateRepo/PrivateRepo.tsx: -------------------------------------------------------------------------------- 1 | import Feedback from 'components/Feedback/Feedback'; 2 | import React from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | interface PrivateProps { 6 | url: string; 7 | } 8 | 9 | const PrivateRepo = ({ url }: PrivateProps) => { 10 | const { t } = useTranslation(); 11 | return ( 12 |
13 |
{t('private-prompt')}
14 | 15 |
16 | ); 17 | }; 18 | export default PrivateRepo; 19 | -------------------------------------------------------------------------------- /src/components/Radio/Radio.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import * as RadioGroup from '@radix-ui/react-radio-group'; 3 | import storage from '../../utils/storage'; 4 | 5 | interface ToggleGroupProps { 6 | title: string; 7 | // subTitle: string; 8 | 9 | storageKey: string; 10 | defaultValue: string; 11 | options: string[]; 12 | labels: string[]; 13 | imgLink: string[]; 14 | } 15 | 16 | const Radio = ({ storageKey, title, defaultValue, options, labels, imgLink }: ToggleGroupProps) => { 17 | const [value, setValue] = useState('default'); 18 | 19 | useEffect(() => { 20 | storage.get([storageKey]).then((result) => { 21 | if (result[storageKey] === undefined) { 22 | storage.set({ [storageKey]: defaultValue }); 23 | setValue(defaultValue); 24 | } else { 25 | setValue(result[storageKey]); 26 | } 27 | }); 28 | }, []); 29 | 30 | const handleGroupChange = (value: string) => { 31 | storage.set({ [storageKey]: value }); 32 | setValue(value); 33 | }; 34 | return ( 35 |
36 |
{title}
37 | 44 | {labels.map((label, index) => { 45 | return ( 46 |
47 | 48 | 49 | 50 | 53 |
54 | ); 55 | })} 56 |
57 | 58 |
59 | 60 |
61 |
62 | ); 63 | }; 64 | 65 | export default Radio; 66 | -------------------------------------------------------------------------------- /src/content/TechStacks.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useLayoutEffect, useState } from 'react'; 2 | import FingerprintJS from '@fingerprintjs/fingerprintjs'; 3 | import Category from '../components/Category/Category'; 4 | import Feedback from 'components/Feedback/Feedback'; 5 | import useSWR from 'swr'; 6 | import Loading from 'components/Loading/Loading'; 7 | import Failed from 'components/Failed/Failed'; 8 | 9 | import { useTranslation } from 'react-i18next'; 10 | import AnimateHeight, { Height } from 'react-animate-height'; 11 | import storage from '../utils/storage'; 12 | 13 | const VERSION = '1.25'; 14 | 15 | async function fetcher(input: RequestInfo, init?: RequestInit): Promise { 16 | const res = await fetch(input, { credentials: 'include', ...init }); 17 | return res.json(); 18 | } 19 | interface TechStacksProps { 20 | url: string; 21 | } 22 | 23 | const reportVersionRequest = async (id: string) => { 24 | fetch('https://techstack.zeabur.app/report', { 25 | method: 'POST', 26 | credentials: 'include', 27 | body: JSON.stringify({ 28 | version: VERSION, 29 | id: id, 30 | }), 31 | headers: { 32 | 'Content-type': 'application/json; charset=UTF-8', 33 | }, 34 | }).catch((err) => { 35 | console.log(err); 36 | }); 37 | }; 38 | 39 | const TechStacks = ({ url }: TechStacksProps) => { 40 | const { data, error, isLoading } = useSWR(`https://techstack.zeabur.app/repo?url=${url}`, { 41 | fetcher: fetcher, 42 | refreshInterval: 0, 43 | revalidateOnFocus: false, 44 | }); 45 | 46 | const techstackRef = useRef(null); 47 | 48 | const [expand, setExpand] = useState(false); 49 | const [height, setHeight] = useState(0); 50 | 51 | // the status is to renew get the height of the techstack container 52 | const [loadStatus, setLoadStatus] = useState(false); 53 | 54 | const open = () => { 55 | setExpand(true); 56 | }; 57 | 58 | const { t } = useTranslation(); 59 | 60 | useEffect(() => { 61 | const reportVersion = async () => { 62 | const fp = await FingerprintJS.load(); 63 | 64 | const { visitorId } = await fp.get(); 65 | // report extension version 66 | if (sessionStorage.getItem('id') !== visitorId) { 67 | reportVersionRequest(visitorId); 68 | sessionStorage.setItem('id', visitorId); 69 | } 70 | }; 71 | 72 | storage.get(['allow_report_version']).then((result) => { 73 | if (result['allow_report_version'] || result['allow_report_version'] === undefined) { 74 | reportVersion(); 75 | } 76 | }); 77 | }, []); 78 | 79 | useEffect(() => { 80 | storage.get(['auto_collapsed']).then((result) => { 81 | if (result['auto_collapsed'] || result['auto_collapsed'] == undefined) { 82 | setExpand(false); 83 | } else { 84 | setExpand(true); 85 | } 86 | }); 87 | }, []); 88 | 89 | useLayoutEffect(() => { 90 | const height = techstackRef?.current?.offsetHeight; 91 | if (!height) { 92 | return; 93 | } 94 | setHeight(height); 95 | }, [loadStatus]); 96 | 97 | if (error) return ; 98 | 99 | if (isLoading) return ; 100 | 101 | const results: any[] = []; 102 | Object.entries(data || {}).forEach(([key, value]) => { 103 | results.push(); 104 | }); 105 | 106 | // the status is for to trigger the useEffect to get the height of the techstack container 107 | if (!loadStatus) { 108 | setLoadStatus(true); 109 | } 110 | 111 | return ( 112 |
117 | {!expand && !(height < 450) && ( 118 |
119 |
open()}> 120 | {/* */} 121 |
{t('expand-all')}
122 |
123 |
124 | )} 125 | 126 |
127 | {results.length !== 0 && results} 128 | {results.length === 0 &&
{t('no-found-prompt')}
} 129 |
130 | 131 |
132 | ); 133 | }; 134 | 135 | const TechstackAnimation = ({ url }: TechStacksProps) => { 136 | const [height, setHeight] = useState('auto'); 137 | const contentDiv = useRef(null); 138 | 139 | useEffect(() => { 140 | const resizeObserver = new ResizeObserver(() => { 141 | setHeight(contentDiv?.current?.clientHeight || 'auto'); 142 | }); 143 | 144 | resizeObserver.observe(contentDiv?.current || document.body); 145 | 146 | return () => resizeObserver.disconnect(); 147 | }, []); 148 | 149 | return ( 150 | 151 | 152 | 153 | ); 154 | }; 155 | export default TechstackAnimation; 156 | -------------------------------------------------------------------------------- /src/content/content.css: -------------------------------------------------------------------------------- 1 | .techstack-title { 2 | color: #656d76; 3 | font-size: 14px; 4 | line-height: 20px; 5 | margin-bottom: 4px; 6 | } 7 | 8 | .techstack-deps { 9 | display: flex; 10 | flex-wrap: wrap; 11 | gap: 4px 12px; 12 | } 13 | 14 | .techstack-category-container { 15 | display: flex; 16 | flex-direction: column; 17 | gap: 6px; 18 | } 19 | 20 | .techstack-link { 21 | color: #656d76; 22 | display: flex; 23 | gap: 4px; 24 | align-items: center; 25 | } 26 | 27 | .techstack-link:hover { 28 | color: #1f6fe5; 29 | text-decoration: none; 30 | } 31 | 32 | .techstack-name { 33 | margin-top: auto; 34 | margin-bottom: auto; 35 | } 36 | 37 | .techstack-icon { 38 | width: 16px; 39 | height: 16px; 40 | border-radius: 2px; 41 | object-fit: cover; 42 | } 43 | 44 | .techStackRoot { 45 | padding-top: 8px; 46 | padding-bottom: 15px; 47 | border-bottom: 1px solid var(--color-border-muted); 48 | 49 | position: relative; 50 | } 51 | 52 | .techstack-collapsed-container { 53 | position: absolute; 54 | /* display: flex; */ 55 | bottom: 0px; 56 | /* top: 100px; */ 57 | /* background: linear-gradient(top, rgba(0, 0, 0, 0.1), white); */ 58 | background: linear-gradient(var(--color-project-gradient-out), var(--color-project-sidebar-bg)); 59 | 60 | width: 100%; 61 | height: 32px; 62 | 63 | /* justify-content: space-between; 64 | align-items: center; */ 65 | } 66 | 67 | .techstack-open { 68 | width: 100%; 69 | text-align: center; 70 | display: flex; 71 | gap: 4px; 72 | align-items: center; 73 | justify-content: center; 74 | font-size: 13px; 75 | /* margin: auto; */ 76 | cursor: pointer; 77 | } 78 | .techstack-open-title { 79 | color: #0969da; 80 | padding: 2px 10px 2px 10px; 81 | border-radius: 10px; 82 | background: #ddf4ff; 83 | } 84 | 85 | @media (prefers-color-scheme: dark) { 86 | .techstack-open { 87 | /* background: #e6edf3; */ 88 | } 89 | } 90 | 91 | @media (prefers-color-scheme: dark) { 92 | .techstack-open-title { 93 | background: #388bfd1a; 94 | color: #2f81f7; 95 | } 96 | } 97 | 98 | @keyframes jump-animation { 99 | 0% { 100 | transform: translate3d(0, 0, 0) scale3d(1, 1, 1); 101 | } 102 | 40% { 103 | transform: translate3d(0, 30%, 0) scale3d(0.7, 2, 1); 104 | } 105 | 100% { 106 | transform: translate3d(0, 100%, 0) scale3d(1.5, 0.7, 1); 107 | } 108 | } 109 | .tech-stack-jump-animation { 110 | transform-origin: 50% 50%; 111 | animation: jump-animation 2s linear alternate infinite; 112 | } 113 | 114 | .techstack-footer { 115 | display: flex; 116 | width: 100%; 117 | flex-direction: column; 118 | align-items: center; 119 | justify-content: center; 120 | padding: 2px; 121 | background-color: var(--color-bg-footer); 122 | color: var(--color-text-muted); 123 | font-size: 14px; 124 | text-align: center; 125 | } 126 | .techstack-feedback { 127 | display: flex; 128 | margin: auto; 129 | display: flex; 130 | width: 100%; 131 | max-width: 400px; 132 | } 133 | 134 | .techstack-feedback-interaction { 135 | display: flex; 136 | flex-direction: row; 137 | gap: 5px; 138 | margin: auto; 139 | } 140 | 141 | .techstack-footer-feedback { 142 | margin-left: auto; 143 | margin-right: auto; 144 | } 145 | 146 | @media (prefers-color-scheme: dark) { 147 | .techstack-title { 148 | color: #7d8590; 149 | font-size: 14px; 150 | line-height: 20px; 151 | margin-bottom: 4px; 152 | } 153 | 154 | .techstack-link { 155 | color: #e6edf3; 156 | } 157 | 158 | .techstack-link:hover { 159 | color: #2f81f7; 160 | text-decoration: none; 161 | } 162 | 163 | .techstack-open { 164 | color: #7d8590; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/content/index.tsx: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom/client'; 4 | import { runtime } from 'webextension-polyfill'; 5 | import MessageListener from './messageListener'; 6 | import TechStacks from './TechStacks'; 7 | import isGithubRepoPage from './isGithubRepoPage'; 8 | 9 | import PrivateRepo from 'components/PrivateRepo/PrivateRepo'; 10 | import storage from '../utils/storage'; 11 | 12 | import '../i18n'; 13 | 14 | runtime.onMessage.addListener(MessageListener); 15 | 16 | const get_insert_dom = async () => { 17 | const result = await storage.get(['techstack_position']); 18 | let position = 0; 19 | 20 | if (result['techstack_position'] === 'top' || result['techstack_position'] === undefined) { 21 | position = 0; 22 | } 23 | 24 | if (result['techstack_position'] === 'middle') { 25 | position = 1; 26 | } 27 | 28 | if (result['techstack_position'] === 'bottom') { 29 | position = 3; 30 | } 31 | 32 | const dom = document.getElementsByClassName('BorderGrid-cell')[position]; 33 | return dom; 34 | }; 35 | 36 | async function injectComponent() { 37 | console.log('Tech-Stack: https://github.com/Get-Tech-Stack/TechStack'); 38 | 39 | const url = window.location.href; 40 | 41 | const techStack = `

Tech Stack

42 |
43 |
44 |
45 |
`; 46 | 47 | const dom = await get_insert_dom(); 48 | if (dom !== undefined) { 49 | let ReactDom = ; 50 | 51 | let repoStatusDom = document 52 | .getElementsByClassName(`Label Label--secondary v-align-middle mr-1 d-none d-md-block`) 53 | .item(0); 54 | 55 | if (repoStatusDom === undefined || repoStatusDom === null) { 56 | repoStatusDom = document.getElementsByClassName(`Label Label--secondary v-align-middle mr-1`).item(0); 57 | } 58 | 59 | if (repoStatusDom === undefined || repoStatusDom === null) { 60 | repoStatusDom = document 61 | .getElementsByClassName(`Label Label--attention v-align-middle mr-1 d-none d-md-block`) 62 | .item(0); 63 | } 64 | 65 | if (repoStatusDom !== undefined && repoStatusDom !== null) { 66 | if (repoStatusDom.innerHTML === 'Private') { 67 | ReactDom = ; 68 | } else { 69 | ReactDom = ; 70 | } 71 | } 72 | 73 | if (document.getElementById('techStackRoot') === null) { 74 | dom.innerHTML = techStack + dom.innerHTML; 75 | const root = ReactDOM.createRoot(document.getElementById('techStackRoot') || document.createElement('div')); 76 | root.render(ReactDom); 77 | } 78 | } 79 | } 80 | 81 | window.addEventListener('reject', () => { 82 | const url = window.location.href; 83 | // to verify the url is github repo page 84 | // if url like https://github.com/Gepsonka/TDK in regex 85 | if (!isGithubRepoPage(url)) { 86 | return; 87 | } 88 | 89 | injectComponent(); 90 | }); 91 | -------------------------------------------------------------------------------- /src/content/isGithubRepoPage.ts: -------------------------------------------------------------------------------- 1 | export default function isGithubRepoPage(url: string): boolean { 2 | const regex: RegExp = /^https:\/\/github\.com\/[A-Za-z0-9_-]+\/[A-Za-z0-9_.-]+$/; 3 | return regex.test(url); 4 | } 5 | -------------------------------------------------------------------------------- /src/content/messageListener.ts: -------------------------------------------------------------------------------- 1 | import { Runtime } from 'webextension-polyfill'; 2 | 3 | export const onRequest = async ( 4 | msg: EXTMessage, 5 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | sender: Runtime.SendMessageOptionsType, 7 | ): Promise => { 8 | try { 9 | switch (msg.type) { 10 | // call inject component code when tab is updated 11 | case 'REJECT': { 12 | const event = new Event('reject'); 13 | window.dispatchEvent(event); 14 | return { type: 'SUCCESS' }; 15 | } 16 | default: 17 | return { type: 'SUCCESS' }; 18 | } 19 | } catch (error) { 20 | throw error; 21 | } 22 | }; 23 | 24 | export default onRequest; 25 | -------------------------------------------------------------------------------- /src/content/test.js: -------------------------------------------------------------------------------- 1 | import isGithubRepoPage from './isGithubRepoPage'; 2 | 3 | asserts(isGithubRepoPage('https://github.com/reactjs/zh-hans.react.dev'), true); 4 | asserts(isGithubRepoPage('https://github.com/cypress-io/cypress'), true); 5 | asserts(isGithubRepoPage('https://github.com/'), false); 6 | asserts(isGithubRepoPage('https://github.com/cypress-io'), false); 7 | -------------------------------------------------------------------------------- /src/i18n.js: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | 4 | import Backend from 'i18next-http-backend'; 5 | import LanguageDetector from 'i18next-browser-languagedetector'; 6 | // don't want to use this? 7 | // have a look at the Quick start guide 8 | // for passing in lng and translations on init 9 | 10 | i18n 11 | .use(Backend) 12 | .use(LanguageDetector) 13 | .use(initReactI18next) 14 | .init({ 15 | fallbackLng: 'en', 16 | debug: false, 17 | react: { 18 | useSuspense: false, 19 | }, 20 | backend: { 21 | loadPath: 'https://techstacki18n.zeabur.app/{{lng}}.json', 22 | parse: (data) => JSON.parse(data), 23 | }, 24 | interpolation: { 25 | escapeValue: false, // not needed for react as it escapes by default 26 | }, 27 | }); 28 | 29 | export default i18n; 30 | -------------------------------------------------------------------------------- /src/options/Container.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface ContainerProps { 4 | title: string; 5 | children: React.ReactNode; 6 | } 7 | const Container = ({ title, children }: ContainerProps) => { 8 | return ( 9 |
10 |

{title}

11 |
{children}
12 |
13 | ); 14 | }; 15 | 16 | export default Container; 17 | -------------------------------------------------------------------------------- /src/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Options & Settings 19 | 20 | 21 | 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /src/options/options.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f5f5f5; 3 | height: 100vh; 4 | width: 100vw; 5 | margin: 0; 6 | } 7 | 8 | .techstack-checkbox-root { 9 | display: flex; 10 | flex-direction: column; 11 | } 12 | .techstack-checkbox { 13 | display: flex; 14 | } 15 | 16 | .techstack-checkbox-title { 17 | display: flex; 18 | flex-direction: column; 19 | } 20 | 21 | .techstack-checkbox-title-primary { 22 | --color-mktg-btn-shadow-hover-muted: rgba(0, 0, 0, 0.7) 0 0 0 2px inset; 23 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif, 24 | 'Apple Color Emoji', 'Segoe UI Emoji'; 25 | font-size: var(--body-font-size, 14px); 26 | line-height: 1.5; 27 | color: var(--fgColor-default, var(--color-fg-default)); 28 | word-wrap: break-word; 29 | --Layout-gutter: 16px; 30 | --Layout-sidebar-width: 256px; 31 | box-sizing: border-box; 32 | font-weight: var(--base-text-weight-semibold, 600); 33 | } 34 | 35 | .techstack-checkbox-title-second { 36 | --color-mktg-btn-shadow-hover-muted: rgba(0, 0, 0, 0.7) 0 0 0 2px inset; 37 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif, 38 | 'Apple Color Emoji', 'Segoe UI Emoji'; 39 | line-height: 1.5; 40 | word-wrap: break-word; 41 | --Layout-gutter: 16px; 42 | --Layout-sidebar-width: 256px; 43 | box-sizing: border-box; 44 | min-height: 17px; 45 | display: block; 46 | margin: 0; 47 | font-size: 12px; 48 | font-weight: var(--base-text-weight-normal, 400); 49 | color: var(--fgColor-muted, var(--color-fg-muted)); 50 | } 51 | 52 | .techstack-save-btn { 53 | --color-mktg-btn-shadow-hover-muted: rgba(0, 0, 0, 0.7) 0 0 0 2px inset; 54 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif, 55 | 'Apple Color Emoji', 'Segoe UI Emoji'; 56 | line-height: 1.5; 57 | color: var(--fgColor-default, var(--color-fg-default)); 58 | word-wrap: break-word; 59 | --Layout-gutter: 16px; 60 | --Layout-sidebar-width: 256px; 61 | box-sizing: border-box; 62 | margin-top: 0; 63 | margin-bottom: 0; 64 | flex: 1 1 auto; 65 | font-size: 24px; 66 | font-weight: var(--base-text-weight-normal, 400); 67 | order: 0; 68 | } 69 | 70 | /* reset */ 71 | button { 72 | all: unset; 73 | } 74 | 75 | .RadioGroupRoot { 76 | display: flex; 77 | flex-direction: row; 78 | gap: 10px; 79 | } 80 | 81 | .RadioGroupItem { 82 | background-color: white; 83 | width: 25px; 84 | height: 25px; 85 | border-radius: 100%; 86 | box-shadow: 0 2px 10px #0000002b; 87 | } 88 | .RadioGroupItem:hover { 89 | cursor: pointer; 90 | background-color: #f5f2ff; 91 | } 92 | .RadioGroupItem:focus { 93 | box-shadow: 0 0 0 2px black; 94 | } 95 | 96 | .RadioGroupIndicator { 97 | font-size: 15px; 98 | display: flex; 99 | align-items: center; 100 | justify-content: center; 101 | width: 100%; 102 | height: 100%; 103 | position: relative; 104 | } 105 | 106 | .RadioGroupIndicator::after { 107 | content: ''; 108 | color: black; 109 | display: block; 110 | width: 11px; 111 | height: 11px; 112 | border-radius: 50%; 113 | background-color: black; 114 | } 115 | 116 | .Label { 117 | color: black; 118 | font-size: 15px; 119 | line-height: 1; 120 | padding-left: 15px; 121 | } 122 | 123 | .techstack-img-container { 124 | padding: 20px; 125 | } 126 | .techstack-img { 127 | height: 200px; 128 | width: 200px; 129 | } 130 | 131 | .techstack-container { 132 | padding-left: 20px; 133 | margin: 15px 0; 134 | } 135 | 136 | .techstack-container-children { 137 | padding-left: 20px; 138 | margin: 15px 0; 139 | display: flex; 140 | flex-direction: column; 141 | gap: 20px; 142 | } 143 | 144 | @import '@radix-ui/colors/black-alpha.css'; 145 | 146 | /* reset */ 147 | button { 148 | all: unset; 149 | } 150 | 151 | .SwitchRoot { 152 | width: 42px; 153 | height: 25px; 154 | background-color: #00000072; 155 | border-radius: 9999px; 156 | position: relative; 157 | box-shadow: 0 2px 10px #0000002b; 158 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 159 | } 160 | 161 | .SwitchRoot:hover { 162 | cursor: pointer; 163 | } 164 | .SwitchRoot:focus { 165 | box-shadow: 0 0 0 2px #0000002b; 166 | } 167 | .SwitchRoot[data-state='checked'] { 168 | background-color: black; 169 | } 170 | 171 | .SwitchThumb { 172 | display: block; 173 | width: 21px; 174 | height: 21px; 175 | background-color: white; 176 | border-radius: 9999px; 177 | box-shadow: 0 2px 2px black; 178 | transition: transform 100ms; 179 | transform: translateX(2px); 180 | will-change: transform; 181 | } 182 | .SwitchThumb[data-state='checked'] { 183 | transform: translateX(19px); 184 | } 185 | 186 | .Label { 187 | color: black; 188 | font-size: 15px; 189 | line-height: 1; 190 | } 191 | -------------------------------------------------------------------------------- /src/options/options.tsx: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom/client'; 5 | 6 | import Container from './Container'; 7 | import Check from '../components/Check/Check'; 8 | import Radio from '../components/Radio/Radio'; 9 | import '../i18n'; 10 | import { useTranslation } from 'react-i18next'; 11 | 12 | const Index = () => { 13 | const { t } = useTranslation(); 14 | 15 | return ( 16 |
17 | 18 | 24 | 25 | 26 | 32 | 38 | 46 | 47 | 48 | {/* */} 49 | {/* */} 50 |
51 | ); 52 | }; 53 | 54 | const root = ReactDOM.createRoot(document.getElementById('display-container')!); 55 | root.render(); 56 | -------------------------------------------------------------------------------- /src/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/popup/index.tsx: -------------------------------------------------------------------------------- 1 | chrome.tabs.create({ 2 | active: true, 3 | url: 'https://github.com/Get-Tech-Stack/TechStack', 4 | }); 5 | 6 | console.log('hello world'); 7 | // try { 8 | // browser.tabs.create({ 9 | // url: "https://github.com/Get-Tech-Stack/TechStack", 10 | // }); 11 | // } catch (e) { 12 | // console.log(e) 13 | // } 14 | -------------------------------------------------------------------------------- /src/types/message.d.ts: -------------------------------------------------------------------------------- 1 | declare type EXTMessageType = 'CHANGE_COLOR' | 'REJECT' | 'SUCCESS' | 'ERROR'; 2 | 3 | declare type EXTMessage = { 4 | type: EXTMessageType; 5 | data?: T; 6 | }; 7 | -------------------------------------------------------------------------------- /src/types/openControl.d.ts: -------------------------------------------------------------------------------- 1 | declare type OpenControls = { 2 | isVisible: boolean; 3 | handleClose: () => void; 4 | handleOpen: () => void; 5 | handleToggle: () => void; 6 | isOpen: boolean; 7 | }; 8 | -------------------------------------------------------------------------------- /src/types/response.d.ts: -------------------------------------------------------------------------------- 1 | declare type EXTResponseType = 'SUCCESS' | 'FAILED' | 'PENDING' | 'UNAUTHORIZED' | 'AUTHENTICATED'; 2 | 3 | declare type EXTResponse = { 4 | type: EXTResponseType; 5 | data?: T; 6 | }; 7 | -------------------------------------------------------------------------------- /src/types/techstack.ts: -------------------------------------------------------------------------------- 1 | interface TechStack { 2 | id: number; 3 | } 4 | 5 | export default TechStack; 6 | -------------------------------------------------------------------------------- /src/utils/get_option.ts: -------------------------------------------------------------------------------- 1 | import storage from './storage'; 2 | 3 | // WIP 4 | const get_options = (key: string): boolean => { 5 | storage.get([key]); 6 | return true; 7 | }; 8 | 9 | export default get_options; 10 | -------------------------------------------------------------------------------- /src/utils/isGithubRepoPage.ts: -------------------------------------------------------------------------------- 1 | export default function isGithubRepoPage(url: string): boolean { 2 | const regex: RegExp = /^https:\/\/github\.com\/[A-Za-z0-9_-]+\/[A-Za-z0-9_.-]+$/; 3 | return regex.test(url); 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/isGithubRepoPage_test.js: -------------------------------------------------------------------------------- 1 | import isGithubRepoPage from './isGithubRepoPage'; 2 | 3 | asserts(isGithubRepoPage('https://github.com/reactjs/zh-hans.react.dev'), true); 4 | asserts(isGithubRepoPage('https://github.com/cypress-io/cypress'), true); 5 | asserts(isGithubRepoPage('https://github.com/'), false); 6 | asserts(isGithubRepoPage('https://github.com/cypress-io'), false); 7 | -------------------------------------------------------------------------------- /src/utils/sendMessages.ts: -------------------------------------------------------------------------------- 1 | import { runtime, tabs, Runtime, Tabs } from 'webextension-polyfill'; 2 | 3 | /** 4 | * Send Message to Background Script 5 | * 6 | * @param msg 7 | * @returns 8 | */ 9 | export const sendMessage = (msg: EXTMessage, options?: Runtime.SendMessageOptionsType): Promise => { 10 | return runtime.sendMessage(msg, options); 11 | }; 12 | 13 | /** 14 | * Send Message to Content Script 15 | */ 16 | export const sendMessageToTab = ( 17 | tab: Tabs.Tab, 18 | msg: EXTMessage, 19 | options?: Tabs.SendMessageOptionsType 20 | ): Promise => { 21 | return tabs.sendMessage(tab.id as number, msg, options); 22 | }; 23 | 24 | /** 25 | * Send Message to Content Script 26 | */ 27 | export const sendMessageToActiveTab = async ( 28 | msg: EXTMessage, 29 | options?: Tabs.SendMessageOptionsType 30 | ): Promise => { 31 | let activeTab: Tabs.Tab; 32 | try { 33 | const activeTabs = await tabs.query({ active: true, currentWindow: true }); 34 | activeTab = activeTabs[0]; 35 | } catch (error) { 36 | console.log('[===== Error in sendMessageToActiveTab =====]', error); 37 | throw `Error in sendMessageToActiveTab`; 38 | } 39 | return sendMessageToTab(activeTab, msg, options); 40 | }; 41 | -------------------------------------------------------------------------------- /src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | import { storage } from 'webextension-polyfill'; 2 | 3 | export default storage.sync ? storage.sync : storage.local; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "strictNullChecks": true, 5 | "noImplicitThis": true, 6 | "baseUrl": "./src", 7 | "esModuleInterop": true, 8 | "module": "commonjs", 9 | "target": "es6", 10 | "allowJs": true, 11 | "jsx": "react", 12 | "sourceMap": true, 13 | "moduleResolution": "node", 14 | "allowSyntheticDefaultImports": true, 15 | "allowUmdGlobalAccess": true, 16 | "resolveJsonModule": true, 17 | "paths": { 18 | "utils/*": ["./utils/*"], 19 | "popup/*": ["./popup/*"], 20 | "background/*": ["./background/*"], 21 | "options/*": ["./options/*"], 22 | "content/*": ["./content/*"], 23 | "assets/*": ["./assets/*"], 24 | "components/*": ["./components/*"], 25 | "types/*": ["./types/*"], 26 | "@redux/*": ["./@redux/*"], 27 | "@hooks/*": ["./hooks/*"] 28 | }, 29 | "typeRoots": ["node_modules/@types", "./src/types"] 30 | }, 31 | "exclude": ["dist", "dev", "temp"] 32 | } 33 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import TerserPlugin from 'terser-webpack-plugin'; 2 | 3 | import { 4 | getHTMLPlugins, 5 | getOutput, 6 | getCopyPlugins, 7 | getZipPlugins, 8 | getEntry, 9 | getResolves, 10 | getDefinePlugins, 11 | getCleanWebpackPlugins, 12 | config, 13 | getExtensionManifestPlugins, 14 | getEslintPlugins, 15 | getProgressPlugins, 16 | getExtensionReloaderPlugins, 17 | Directories, 18 | getAnalyzerPlugins, 19 | } from './webpack.config.utils'; 20 | 21 | let generalConfig: any = { 22 | mode: config.NODE_ENV === 'production' || config.NODE_ENV === 'upload' ? 'production' : 'development', 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.(js|jsx|ts|tsx)$/, 27 | use: [ 28 | { 29 | loader: 'ts-loader', 30 | // options: { 31 | // transpileOnly: true, 32 | // }, 33 | }, 34 | ], 35 | exclude: /node_modules/, 36 | }, 37 | ], 38 | }, 39 | resolve: getResolves(), 40 | entry: getEntry(Directories.SRC_DIR), 41 | output: getOutput(config.TARGET, config.OUTPUT_DIR), 42 | }; 43 | 44 | let plugins: any[] = [ 45 | ...getCleanWebpackPlugins(`${config.OUTPUT_DIR}/${config.TARGET}`, `${Directories.DIST_DIR}/${config.TARGET}`), 46 | ...getProgressPlugins(), 47 | ...getEslintPlugins(), 48 | ...getDefinePlugins(), 49 | ...getExtensionManifestPlugins(), 50 | ...getHTMLPlugins(config.TARGET, config.OUTPUT_DIR, Directories.SRC_DIR), 51 | ...getCopyPlugins(config.TARGET, config.OUTPUT_DIR, Directories.SRC_DIR), 52 | ]; 53 | 54 | if (config.NODE_ENV === 'development') { 55 | generalConfig = { 56 | ...generalConfig, 57 | devtool: 'source-map', 58 | stats: { 59 | all: false, 60 | builtAt: true, 61 | errors: true, 62 | hash: true, 63 | }, 64 | watch: true, 65 | watchOptions: { 66 | aggregateTimeout: 200, 67 | poll: 1000, 68 | }, 69 | }; 70 | 71 | plugins = [...plugins, ...getExtensionReloaderPlugins()]; 72 | } 73 | 74 | if (config.NODE_ENV === 'profile') { 75 | generalConfig = { 76 | ...generalConfig, 77 | devtool: 'source-map', 78 | stats: { 79 | all: false, 80 | builtAt: true, 81 | errors: true, 82 | hash: true, 83 | }, 84 | }; 85 | 86 | plugins = [...plugins, ...getAnalyzerPlugins()]; 87 | } 88 | 89 | if (config.NODE_ENV === 'upload') { 90 | generalConfig = { 91 | ...generalConfig, 92 | optimization: { 93 | minimize: true, 94 | minimizer: [ 95 | new TerserPlugin({ 96 | parallel: true, 97 | terserOptions: { 98 | format: { 99 | comments: true, 100 | }, 101 | }, 102 | extractComments: true, 103 | }), 104 | ], 105 | }, 106 | }; 107 | 108 | plugins = [...plugins]; 109 | } 110 | 111 | if (config.NODE_ENV === 'production') { 112 | generalConfig = { 113 | ...generalConfig, 114 | optimization: { 115 | minimize: false, 116 | minimizer: [ 117 | new TerserPlugin({ 118 | parallel: true, 119 | terserOptions: { 120 | format: { 121 | comments: true, 122 | }, 123 | }, 124 | extractComments: true, 125 | }), 126 | ], 127 | }, 128 | }; 129 | 130 | plugins = [...plugins, ...getZipPlugins(config.TARGET, Directories.DIST_DIR)]; 131 | } 132 | 133 | export default [ 134 | { 135 | ...generalConfig, 136 | plugins, 137 | }, 138 | ]; 139 | -------------------------------------------------------------------------------- /webpack.config.utils.ts: -------------------------------------------------------------------------------- 1 | import { ProgressPlugin, DefinePlugin } from 'webpack'; 2 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 3 | import CopyWebpackPlugin from 'copy-webpack-plugin'; 4 | import ZipPlugin from 'zip-webpack-plugin'; 5 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 6 | 7 | import path from 'path'; 8 | import { CleanWebpackPlugin } from 'clean-webpack-plugin'; 9 | import ESLintPlugin from 'eslint-webpack-plugin'; 10 | import WebpackExtensionManifestPlugin from 'webpack-extension-manifest-plugin'; 11 | 12 | const ExtReloader = require('webpack-ext-reloader-mv3'); 13 | 14 | const baseManifestChrome = require('./src/baseManifest_chrome.json'); 15 | const baseManifestFirefox = require('./src/baseManifest_firefox.json'); 16 | const baseManifestOpera = require('./src/baseManifest_opera.json'); 17 | const baseManifestEdge = require('./src/baseManifest_edge.json'); 18 | 19 | const baseManifest = { 20 | chrome: baseManifestChrome, 21 | firefox: baseManifestFirefox, 22 | opera: baseManifestOpera, 23 | edge: baseManifestEdge, 24 | }; 25 | 26 | const dotenv = require('dotenv').config({ path: __dirname + '/.env' }); 27 | 28 | interface EnvironmentConfig { 29 | NODE_ENV: string; 30 | OUTPUT_DIR: string; 31 | TARGET: string; 32 | } 33 | 34 | export const Directories = { 35 | DEV_DIR: 'dev', 36 | DIST_DIR: 'dist', 37 | TEMP_DIR: 'temp', 38 | SRC_DIR: 'src', 39 | }; 40 | 41 | /** 42 | * Environment Config 43 | * 44 | */ 45 | const EnvConfig: EnvironmentConfig = { 46 | OUTPUT_DIR: 47 | process.env.NODE_ENV === 'production' 48 | ? Directories.TEMP_DIR 49 | : process.env.NODE_ENV === 'upload' 50 | ? Directories.DIST_DIR 51 | : Directories.DEV_DIR, 52 | ...(process.env.NODE_ENV ? { NODE_ENV: process.env.NODE_ENV } : { NODE_ENV: 'development' }), 53 | ...(process.env.TARGET ? { TARGET: process.env.TARGET } : { TARGET: 'chrome' }), 54 | }; 55 | 56 | /** 57 | * Get HTML Plugins 58 | * 59 | * @param browserDir 60 | * @param outputDir 61 | * @param sourceDir 62 | * @returns 63 | */ 64 | export const getHTMLPlugins = ( 65 | browserDir: string, 66 | outputDir = Directories.DEV_DIR, 67 | sourceDir = Directories.SRC_DIR, 68 | ) => [ 69 | new HtmlWebpackPlugin({ 70 | title: 'Popup', 71 | filename: path.resolve(__dirname, `${outputDir}/${browserDir}/popup/index.html`), 72 | template: path.resolve(__dirname, `${sourceDir}/popup/index.html`), 73 | chunks: ['popup'], 74 | }), 75 | new HtmlWebpackPlugin({ 76 | title: 'Options', 77 | filename: path.resolve(__dirname, `${outputDir}/${browserDir}/options/index.html`), 78 | template: path.resolve(__dirname, `${sourceDir}/options/index.html`), 79 | chunks: ['options'], 80 | }), 81 | ]; 82 | 83 | /** 84 | * Get DefinePlugins 85 | * 86 | * @param config 87 | * @returns 88 | */ 89 | export const getDefinePlugins = (config = {}) => [ 90 | new DefinePlugin({ 91 | 'process.env': JSON.stringify({ ...config, ...(dotenv.parsed ?? {}) }), 92 | }), 93 | ]; 94 | 95 | /** 96 | * Get Output Configurations 97 | * 98 | * @param browserDir 99 | * @param outputDir 100 | * @returns 101 | */ 102 | export const getOutput = (browserDir: string, outputDir = Directories.DEV_DIR) => { 103 | return { 104 | path: path.resolve(process.cwd(), `${outputDir}/${browserDir}`), 105 | filename: '[name]/[name].js', 106 | }; 107 | }; 108 | 109 | /** 110 | * Get Entry Points 111 | * 112 | * @param sourceDir 113 | * @returns 114 | */ 115 | export const getEntry = (sourceDir = Directories.SRC_DIR) => { 116 | return { 117 | popup: [path.resolve(__dirname, `${sourceDir}/popup/index.tsx`)], 118 | options: [path.resolve(__dirname, `${sourceDir}/options/options.tsx`)], 119 | content: [path.resolve(__dirname, `${sourceDir}/content/index.tsx`)], 120 | background: [path.resolve(__dirname, `${sourceDir}/background/index.ts`)], 121 | }; 122 | }; 123 | 124 | /** 125 | * Get CopyPlugins 126 | * 127 | * @param browserDir 128 | * @param outputDir 129 | * @param sourceDir 130 | * @returns 131 | */ 132 | export const getCopyPlugins = ( 133 | browserDir: string, 134 | outputDir = Directories.DEV_DIR, 135 | sourceDir = Directories.SRC_DIR, 136 | ) => { 137 | return [ 138 | new CopyWebpackPlugin({ 139 | patterns: [ 140 | { 141 | from: path.resolve(__dirname, `${sourceDir}/options/options.css`), 142 | to: path.resolve(__dirname, `${outputDir}/${browserDir}/options`), 143 | }, 144 | { 145 | from: path.resolve(__dirname, `${sourceDir}/assets/options/1.png`), 146 | to: path.resolve(__dirname, `${outputDir}/${browserDir}/options`), 147 | }, 148 | { 149 | from: path.resolve(__dirname, `${sourceDir}/assets/options/2.png`), 150 | to: path.resolve(__dirname, `${outputDir}/${browserDir}/options`), 151 | }, 152 | { 153 | from: path.resolve(__dirname, `${sourceDir}/assets/options/3.png`), 154 | to: path.resolve(__dirname, `${outputDir}/${browserDir}/options`), 155 | }, 156 | 157 | { 158 | from: path.resolve(__dirname, `${sourceDir}/content/content.css`), 159 | to: path.resolve(__dirname, `${outputDir}/${browserDir}/content`), 160 | }, 161 | { 162 | from: path.resolve(__dirname, `${sourceDir}/assets`), 163 | to: path.resolve(__dirname, `${outputDir}/${browserDir}/assets`), 164 | }, 165 | { 166 | from: path.resolve(__dirname, `${sourceDir}/_locales`), 167 | to: path.resolve(__dirname, `${outputDir}/${browserDir}/_locales`), 168 | }, 169 | ], 170 | }), 171 | ]; 172 | }; 173 | 174 | /** 175 | * Get ZipPlugins 176 | * 177 | * @param browserDir 178 | * @param outputDir 179 | * @returns 180 | */ 181 | export const getZipPlugins = (browserDir: string, outputDir = Directories.DIST_DIR) => { 182 | return [ 183 | new ZipPlugin({ 184 | path: path.resolve(process.cwd(), `${outputDir}/${browserDir}`), 185 | filename: browserDir, 186 | extension: 'zip', 187 | fileOptions: { 188 | mtime: new Date(), 189 | mode: 0o100664, 190 | compress: true, 191 | forceZip64Format: false, 192 | }, 193 | zipOptions: { 194 | forceZip64Format: false, 195 | }, 196 | }), 197 | ]; 198 | }; 199 | 200 | /** 201 | * Get Analyzer Plugins 202 | * 203 | * @returns 204 | */ 205 | export const getAnalyzerPlugins = () => { 206 | return [ 207 | new BundleAnalyzerPlugin({ 208 | analyzerMode: 'server', 209 | }), 210 | ]; 211 | }; 212 | 213 | /** 214 | * Get CleanWebpackPlugins 215 | * 216 | * @param dirs 217 | * @returns 218 | */ 219 | export const getCleanWebpackPlugins = (...dirs: string[]) => { 220 | return [ 221 | new CleanWebpackPlugin({ 222 | cleanOnceBeforeBuildPatterns: [...dirs?.map((dir) => path.join(process.cwd(), `${dir}`) ?? [])], 223 | cleanStaleWebpackAssets: false, 224 | verbose: true, 225 | }), 226 | ]; 227 | }; 228 | 229 | /** 230 | * Get Resolves 231 | * 232 | * @returns 233 | */ 234 | export const getResolves = () => { 235 | return { 236 | alias: { 237 | utils: path.resolve(__dirname, './src/utils/'), 238 | popup: path.resolve(__dirname, './src/popup/'), 239 | background: path.resolve(__dirname, './src/background/'), 240 | options: path.resolve(__dirname, './src/options/'), 241 | content: path.resolve(__dirname, './src/content/'), 242 | assets: path.resolve(__dirname, './src/assets/'), 243 | components: path.resolve(__dirname, './src/components/'), 244 | types: path.resolve(__dirname, './src/types/'), 245 | hooks: path.resolve(__dirname, './src/hooks/'), 246 | '@redux': path.resolve(__dirname, './src/@redux/'), 247 | }, 248 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 249 | }; 250 | }; 251 | 252 | /** 253 | * Get Extension Manifest Plugins 254 | * 255 | * @returns 256 | */ 257 | export const getExtensionManifestPlugins = () => { 258 | return [ 259 | new WebpackExtensionManifestPlugin({ 260 | config: { base: (baseManifest as any)[EnvConfig.TARGET] }, 261 | }), 262 | ]; 263 | }; 264 | 265 | export const eslintOptions = { 266 | fix: true, 267 | }; 268 | 269 | /** 270 | * Get Eslint Plugins 271 | * 272 | * @returns 273 | */ 274 | export const getEslintPlugins = (options = eslintOptions) => { 275 | return [new ESLintPlugin(options)]; 276 | }; 277 | 278 | /** 279 | * Get Progress Plugins 280 | * 281 | * @returns 282 | */ 283 | export const getProgressPlugins = () => { 284 | return [new ProgressPlugin()]; 285 | }; 286 | 287 | /** 288 | * Environment Configuration Variables 289 | * 290 | */ 291 | export const config = EnvConfig; 292 | 293 | /** 294 | * Get Extension Reloader Plugin 295 | * 296 | * @returns 297 | */ 298 | export const getExtensionReloaderPlugins = () => { 299 | return [ 300 | new ExtReloader({ 301 | port: 9090, 302 | reloadPage: true, 303 | entries: { 304 | contentScript: ['content'], 305 | background: 'background', 306 | extensionPage: ['popup', 'options'], 307 | }, 308 | }), 309 | ]; 310 | }; 311 | --------------------------------------------------------------------------------