├── .babelrc ├── .github ├── FUNDING.yml └── workflows │ ├── cypress.yml │ └── unit.yml ├── .gitignore ├── .nvmrc ├── .storybook ├── addons.js └── config.js ├── LICENSE ├── README.md ├── cypress.json ├── cypress ├── .eslintrc ├── fixtures │ ├── english-monthly.json │ ├── english.json │ ├── javascript-monthly.json │ ├── javascript.json │ ├── trending-2.json │ └── trending.json ├── integration │ ├── dark-mode.js │ ├── empty-state.js │ ├── error-state.js │ ├── feeling-lucky.js │ ├── load-repositories.js │ ├── scroll-icon.js │ └── selectors.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── images ├── 1280x640.jpg ├── 1280x640_dark.jpg ├── 1280x800.jpg ├── 1280x800_dark.jpg ├── design.sketch ├── hero.jpeg ├── hero.svg ├── icon.png ├── screenshot.jpg ├── tile.jpg ├── tile2.jpg └── tile3.jpg ├── package.json ├── public ├── 128.png ├── 16.png ├── 48.png ├── 512.png ├── 512_dark.png ├── ga.js ├── index.html └── manifest.json ├── src ├── App.js ├── Main.js ├── background │ ├── __tests__ │ │ └── startRequest.js │ ├── index.js │ └── startRequest.js ├── components │ ├── BottomIcons.js │ ├── ContentPlaceholder.js │ ├── EmptyState.js │ ├── Fade.js │ ├── Footer.js │ ├── Icon.js │ ├── InfoItem.js │ ├── LanguageSelect.js │ ├── LastUpdated.js │ ├── NetworkError.js │ ├── PeriodSelect.js │ ├── RepositoriesList.js │ ├── RepositoryCard.js │ ├── ScrollTop.js │ ├── Select.js │ ├── SpokenLanguageSelect.js │ ├── TopBar.js │ ├── index.js │ └── stories │ │ ├── Components.stories.js │ │ ├── Icon.stories.js │ │ └── NetworkError.stories.js ├── dev-tools │ ├── dev-tools.js │ └── load.js ├── feature-toggles.js ├── fonts │ ├── index.js │ └── tt-commons │ │ ├── TTCommons-Black.eot │ │ ├── TTCommons-Black.ttf │ │ ├── TTCommons-Black.woff │ │ ├── TTCommons-BlackItalic.eot │ │ ├── TTCommons-BlackItalic.ttf │ │ ├── TTCommons-BlackItalic.woff │ │ ├── TTCommons-Bold.eot │ │ ├── TTCommons-Bold.ttf │ │ ├── TTCommons-Bold.woff │ │ ├── TTCommons-BoldItalic.eot │ │ ├── TTCommons-BoldItalic.ttf │ │ ├── TTCommons-BoldItalic.woff │ │ ├── TTCommons-DemiBold.eot │ │ ├── TTCommons-DemiBold.ttf │ │ ├── TTCommons-DemiBold.woff │ │ ├── TTCommons-DemiBoldItalic.eot │ │ ├── TTCommons-DemiBoldItalic.ttf │ │ ├── TTCommons-DemiBoldItalic.woff │ │ ├── TTCommons-ExtraBold.eot │ │ ├── TTCommons-ExtraBold.ttf │ │ ├── TTCommons-ExtraBold.woff │ │ ├── TTCommons-ExtraBoldItalic.eot │ │ ├── TTCommons-ExtraBoldItalic.ttf │ │ ├── TTCommons-ExtraBoldItalic.woff │ │ ├── TTCommons-ExtraLight.eot │ │ ├── TTCommons-ExtraLight.ttf │ │ ├── TTCommons-ExtraLight.woff │ │ ├── TTCommons-ExtraLightItalic.eot │ │ ├── TTCommons-ExtraLightItalic.ttf │ │ ├── TTCommons-ExtraLightItalic.woff │ │ ├── TTCommons-Italic.eot │ │ ├── TTCommons-Italic.ttf │ │ ├── TTCommons-Italic.woff │ │ ├── TTCommons-Light.eot │ │ ├── TTCommons-Light.ttf │ │ ├── TTCommons-Light.woff │ │ ├── TTCommons-LightItalic.eot │ │ ├── TTCommons-LightItalic.ttf │ │ ├── TTCommons-LightItalic.woff │ │ ├── TTCommons-Medium.eot │ │ ├── TTCommons-Medium.ttf │ │ ├── TTCommons-Medium.woff │ │ ├── TTCommons-MediumItalic.eot │ │ ├── TTCommons-MediumItalic.ttf │ │ ├── TTCommons-MediumItalic.woff │ │ ├── TTCommons-Regular.eot │ │ ├── TTCommons-Regular.ttf │ │ ├── TTCommons-Regular.woff │ │ ├── TTCommons-Thin.eot │ │ ├── TTCommons-Thin.ttf │ │ ├── TTCommons-Thin.woff │ │ ├── TTCommons-ThinItalic.eot │ │ ├── TTCommons-ThinItalic.ttf │ │ ├── TTCommons-ThinItalic.woff │ │ └── style.css ├── global.css ├── helpers │ ├── github.js │ └── localStorage.js ├── hooks.js ├── hooks │ ├── useLocalStorage.js │ └── useWindowScroll.js ├── images │ ├── author.svg │ ├── chrome.svg │ ├── cross.svg │ ├── empty.svg │ ├── forks.svg │ ├── github.svg │ ├── heart.svg │ ├── logo.svg │ ├── moon.svg │ ├── random.svg │ ├── repos.svg │ ├── setting.svg │ ├── star-filled.svg │ ├── sun.svg │ ├── top.svg │ └── warning.svg ├── index.js ├── setupTests.js └── theme.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-app"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://www.buymeacoffee.com/huchenme'] 13 | -------------------------------------------------------------------------------- /.github/workflows/cypress.yml: -------------------------------------------------------------------------------- 1 | name: Cypress Test 2 | on: [push] 3 | jobs: 4 | cypress-run: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v2 9 | - name: Setup Node 10 | uses: actions/setup-node@v1 11 | with: 12 | node-version: 12.x 13 | - name: Install packages 14 | run: yarn install 15 | - name: Cypress run 16 | uses: cypress-io/github-action@v1 17 | with: 18 | record: true 19 | start: yarn start:nobrowser 20 | wait-on: 'http://localhost:3000' 21 | env: 22 | CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /.github/workflows/unit.yml: -------------------------------------------------------------------------------- 1 | name: Unit test and build 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v2 9 | - name: Setup Node 10 | uses: actions/setup-node@v1 11 | with: 12 | node-version: 12.x 13 | - run: yarn install 14 | - run: yarn build 15 | - run: yarn test 16 | env: 17 | CI: true 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /*.zip 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | /cypress/videos 27 | /cypress/screenshots 28 | 29 | /storybook-static 30 | *.local.js 31 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v12.10.0 2 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | import '@storybook/addon-notes/register'; 4 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure, addDecorator } from '@storybook/react'; 2 | import centered from '@storybook/addon-centered/react'; 3 | 4 | import '../src/global.css'; 5 | 6 | // automatically import all files ending in *.stories.js 7 | const req = require.context('../src', true, /\.stories\.js$/); 8 | function loadStories() { 9 | req.keys().forEach(filename => req(filename)); 10 | } 11 | 12 | addDecorator(centered); 13 | 14 | configure(loadStories, module); 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2019 Hu Chen 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Chrome Web Store](https://img.shields.io/chrome-web-store/v/ibomigipadcieapbemkegkmadbbanbgm.svg?colorB=%234FC828&style=flat)](https://chrome.google.com/webstore/detail/hacker-tab/ibomigipadcieapbemkegkmadbbanbgm) 2 | [![Chrome Web Store Rating](https://img.shields.io/chrome-web-store/stars/ibomigipadcieapbemkegkmadbbanbgm.svg?colorB=%234FC828&label=rating&style=flat)](https://chrome.google.com/webstore/detail/hacker-tab/ibomigipadcieapbemkegkmadbbanbgm/reviews) 3 | [![Travis](https://img.shields.io/travis/huchenme/hacker-tab-extension.svg)](https://travis-ci.org/huchenme/hacker-tab-extension) 4 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/huchenme/hacker-tab-extension/blob/master/LICENSE) 5 | 6 | ## What is Hacker Tab Extension 7 | 8 | Hacker Tab replace browser new tab screen with GitHub trending projects, so that developer get to know trending repositories everyday. It loads trending project periodically in background so you do not need to wait for loading every time you open a new tab. 9 | 10 | ![screenshot](./images/screenshot.jpg) 11 | 12 | ## Install 13 | 14 | 15 | 16 | Trusted by developers! Install Hacker Tab from [Chrome Web Store](https://chrome.google.com/webstore/detail/hacker-tab/ibomigipadcieapbemkegkmadbbanbgm). 17 | 18 | ## View Online 19 | 20 | [View Online](https://hacker-tab-extension.now.sh) version of extension. 21 | 22 | ## Backers 23 | 24 | Thank you to all our backers! 🙏 25 | 26 | Buy Me A Coffee 27 | 28 | ## Feedback 29 | 30 | Just write me an [email](mailto:chen@huchen.dev), or create an [issue](issues). 31 | 32 | ## Give us a rating 33 | 34 | If you enjoy using it, please help to write a review at [Chrome Web Store](https://chrome.google.com/webstore/detail/hacker-tab/ibomigipadcieapbemkegkmadbbanbgm), and star this repo. This will motivate me a lot :) 35 | 36 | ## Related 37 | 38 | - [github-trending-api](https://github.com/huchenme/github-trending-api): The missing APIs for GitHub trending projects and developers. 39 | - [How to use React.js to create a cross-browser extension in 5 minutes](https://levelup.gitconnected.com/how-to-use-react-js-to-create-chrome-extension-in-5-minutes-2ddb11899815?source=friends_link&sk=055e5c73e0dd11fd8cb25130242f388e). 40 | - Hacker Tab on [Product Hunt](https://www.producthunt.com/posts/hacker-tab). 41 | - [Internal Components](https://hacker-tab-components.netlify.com) 42 | 43 | ## Disclaimer 44 | 45 | Hacker Tab is not affiliated with, sponsored by, or endorsed by GitHub Inc. 46 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000", 3 | "projectId": "wrmpdh" 4 | } 5 | -------------------------------------------------------------------------------- /cypress/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { "cypress/globals": true }, 4 | "extends": ["react-app", "plugin:cypress/recommended"], 5 | "plugins": ["cypress"] 6 | } 7 | -------------------------------------------------------------------------------- /cypress/fixtures/trending-2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "author": "bottlerocket-os", 4 | "name": "bottlerocket", 5 | "avatar": "https://github.com/bottlerocket-os.png", 6 | "url": "https://github.com/bottlerocket-os/bottlerocket", 7 | "description": "An operating system designed for hosting containers", 8 | "language": "Rust", 9 | "languageColor": "#dea584", 10 | "stars": 1758, 11 | "forks": 59, 12 | "currentPeriodStars": 1094, 13 | "builtBy": [ 14 | { 15 | "username": "tjkirch", 16 | "href": "https://github.com/tjkirch", 17 | "avatar": "https://avatars3.githubusercontent.com/u/13994" 18 | }, 19 | { 20 | "username": "iliana", 21 | "href": "https://github.com/iliana", 22 | "avatar": "https://avatars3.githubusercontent.com/u/52814" 23 | }, 24 | { 25 | "username": "bcressey", 26 | "href": "https://github.com/bcressey", 27 | "avatar": "https://avatars1.githubusercontent.com/u/174814" 28 | }, 29 | { 30 | "username": "jahkeup", 31 | "href": "https://github.com/jahkeup", 32 | "avatar": "https://avatars2.githubusercontent.com/u/1139752" 33 | }, 34 | { 35 | "username": "etungsten", 36 | "href": "https://github.com/etungsten", 37 | "avatar": "https://avatars2.githubusercontent.com/u/52762042" 38 | } 39 | ] 40 | }, 41 | { 42 | "author": "maxvoltar", 43 | "name": "photo-stream", 44 | "avatar": "https://github.com/maxvoltar.png", 45 | "url": "https://github.com/maxvoltar/photo-stream", 46 | "description": "Self-hosted, super simple photo stream", 47 | "language": "HTML", 48 | "languageColor": "#e34c26", 49 | "stars": 682, 50 | "forks": 93, 51 | "currentPeriodStars": 503, 52 | "builtBy": [ 53 | { 54 | "username": "maxvoltar", 55 | "href": "https://github.com/maxvoltar", 56 | "avatar": "https://avatars3.githubusercontent.com/u/125779" 57 | }, 58 | { 59 | "username": "benubois", 60 | "href": "https://github.com/benubois", 61 | "avatar": "https://avatars2.githubusercontent.com/u/133809" 62 | }, 63 | { 64 | "username": "jadlimcaco", 65 | "href": "https://github.com/jadlimcaco", 66 | "avatar": "https://avatars0.githubusercontent.com/u/949200" 67 | }, 68 | { 69 | "username": "mattsacks", 70 | "href": "https://github.com/mattsacks", 71 | "avatar": "https://avatars3.githubusercontent.com/u/194567" 72 | }, 73 | { 74 | "username": "mikedholt", 75 | "href": "https://github.com/mikedholt", 76 | "avatar": "https://avatars0.githubusercontent.com/u/5240429" 77 | } 78 | ] 79 | }, 80 | { 81 | "author": "pcm-dpc", 82 | "name": "COVID-19", 83 | "avatar": "https://github.com/pcm-dpc.png", 84 | "url": "https://github.com/pcm-dpc/COVID-19", 85 | "description": "COVID-19 Italia - Monitoraggio situazione", 86 | "stars": 1486, 87 | "forks": 284, 88 | "currentPeriodStars": 340, 89 | "builtBy": [ 90 | { 91 | "username": "brucellino", 92 | "href": "https://github.com/brucellino", 93 | "avatar": "https://avatars0.githubusercontent.com/u/2115428" 94 | }, 95 | { 96 | "username": "umbros", 97 | "href": "https://github.com/umbros", 98 | "avatar": "https://avatars1.githubusercontent.com/u/4085151" 99 | } 100 | ] 101 | }, 102 | { 103 | "author": "redwoodjs", 104 | "name": "redwood", 105 | "avatar": "https://github.com/redwoodjs.png", 106 | "url": "https://github.com/redwoodjs/redwood", 107 | "description": "Bringing full-stack to the JAMstack.", 108 | "language": "JavaScript", 109 | "languageColor": "#f1e05a", 110 | "stars": 1898, 111 | "forks": 45, 112 | "currentPeriodStars": 530, 113 | "builtBy": [ 114 | { 115 | "username": "peterp", 116 | "href": "https://github.com/peterp", 117 | "avatar": "https://avatars2.githubusercontent.com/u/44849" 118 | }, 119 | { 120 | "username": "thedavidprice", 121 | "href": "https://github.com/thedavidprice", 122 | "avatar": "https://avatars2.githubusercontent.com/u/2951" 123 | }, 124 | { 125 | "username": "mojombo", 126 | "href": "https://github.com/mojombo", 127 | "avatar": "https://avatars1.githubusercontent.com/u/1" 128 | }, 129 | { 130 | "username": "cannikin", 131 | "href": "https://github.com/cannikin", 132 | "avatar": "https://avatars0.githubusercontent.com/u/300" 133 | }, 134 | { 135 | "username": "adrianmg", 136 | "href": "https://github.com/adrianmg", 137 | "avatar": "https://avatars0.githubusercontent.com/u/589285" 138 | } 139 | ] 140 | }, 141 | { 142 | "author": "google-research", 143 | "name": "google-research", 144 | "avatar": "https://github.com/google-research.png", 145 | "url": "https://github.com/google-research/google-research", 146 | "description": "Google Research", 147 | "language": "Jupyter Notebook", 148 | "languageColor": "#DA5B0B", 149 | "stars": 8356, 150 | "forks": 1488, 151 | "currentPeriodStars": 422, 152 | "builtBy": [ 153 | { 154 | "username": "pdpino", 155 | "href": "https://github.com/pdpino", 156 | "avatar": "https://avatars1.githubusercontent.com/u/20805451" 157 | }, 158 | { 159 | "username": "sun51", 160 | "href": "https://github.com/sun51", 161 | "avatar": "https://avatars3.githubusercontent.com/u/3901200" 162 | }, 163 | { 164 | "username": "agutkin", 165 | "href": "https://github.com/agutkin", 166 | "avatar": "https://avatars0.githubusercontent.com/u/35786058" 167 | }, 168 | { 169 | "username": "dustinvtran", 170 | "href": "https://github.com/dustinvtran", 171 | "avatar": "https://avatars2.githubusercontent.com/u/2569867" 172 | }, 173 | { 174 | "username": "andrewluchen", 175 | "href": "https://github.com/andrewluchen", 176 | "avatar": "https://avatars1.githubusercontent.com/u/6128079" 177 | } 178 | ] 179 | }, 180 | { 181 | "author": "tandasat", 182 | "name": "MiniVisorPkg", 183 | "avatar": "https://github.com/tandasat.png", 184 | "url": "https://github.com/tandasat/MiniVisorPkg", 185 | "description": "The research UEFI hypervisor that supports booting an operating system.", 186 | "language": "C", 187 | "languageColor": "#555555", 188 | "stars": 95, 189 | "forks": 26, 190 | "currentPeriodStars": 36, 191 | "builtBy": [ 192 | { 193 | "username": "tandasat", 194 | "href": "https://github.com/tandasat", 195 | "avatar": "https://avatars1.githubusercontent.com/u/1620923" 196 | }, 197 | { 198 | "username": "brucedang", 199 | "href": "https://github.com/brucedang", 200 | "avatar": "https://avatars3.githubusercontent.com/u/943548" 201 | } 202 | ] 203 | }, 204 | { 205 | "author": "CSSEGISandData", 206 | "name": "COVID-19", 207 | "avatar": "https://github.com/CSSEGISandData.png", 208 | "url": "https://github.com/CSSEGISandData/COVID-19", 209 | "description": "Novel Coronavirus (COVID-19) Cases, provided by JHU CSSE", 210 | "stars": 6953, 211 | "forks": 2467, 212 | "currentPeriodStars": 658, 213 | "builtBy": [ 214 | { 215 | "username": "CSSEGISandData", 216 | "href": "https://github.com/CSSEGISandData", 217 | "avatar": "https://avatars3.githubusercontent.com/u/60674295" 218 | }, 219 | { 220 | "username": "hongru94", 221 | "href": "https://github.com/hongru94", 222 | "avatar": "https://avatars3.githubusercontent.com/u/47940478" 223 | }, 224 | { 225 | "username": "enshengdong", 226 | "href": "https://github.com/enshengdong", 227 | "avatar": "https://avatars0.githubusercontent.com/u/10015024" 228 | } 229 | ] 230 | }, 231 | { 232 | "author": "nndl", 233 | "name": "nndl.github.io", 234 | "avatar": "https://github.com/nndl.png", 235 | "url": "https://github.com/nndl/nndl.github.io", 236 | "description": "《神经网络与深度学习》 邱锡鹏著 Neural Network and Deep Learning", 237 | "language": "HTML", 238 | "languageColor": "#e34c26", 239 | "stars": 11097, 240 | "forks": 2491, 241 | "currentPeriodStars": 56, 242 | "builtBy": [ 243 | { 244 | "username": "xpqiu", 245 | "href": "https://github.com/xpqiu", 246 | "avatar": "https://avatars0.githubusercontent.com/u/6408146" 247 | }, 248 | { 249 | "username": "JerrikEph", 250 | "href": "https://github.com/JerrikEph", 251 | "avatar": "https://avatars0.githubusercontent.com/u/17830427" 252 | }, 253 | { 254 | "username": "QipengGuo", 255 | "href": "https://github.com/QipengGuo", 256 | "avatar": "https://avatars0.githubusercontent.com/u/8382210" 257 | }, 258 | { 259 | "username": "tys1998", 260 | "href": "https://github.com/tys1998", 261 | "avatar": "https://avatars0.githubusercontent.com/u/33472759" 262 | }, 263 | { 264 | "username": "chenkaiyu1997", 265 | "href": "https://github.com/chenkaiyu1997", 266 | "avatar": "https://avatars2.githubusercontent.com/u/16744047" 267 | } 268 | ] 269 | }, 270 | { 271 | "author": "meshtastic", 272 | "name": "Meshtastic-esp32", 273 | "avatar": "https://github.com/meshtastic.png", 274 | "url": "https://github.com/meshtastic/Meshtastic-esp32", 275 | "description": "Device code for the Meshtastic ski/hike/fly/Signal-chat GPS radio", 276 | "language": "C++", 277 | "languageColor": "#f34b7d", 278 | "stars": 337, 279 | "forks": 17, 280 | "currentPeriodStars": 163, 281 | "builtBy": [ 282 | { 283 | "username": "geeksville", 284 | "href": "https://github.com/geeksville", 285 | "avatar": "https://avatars3.githubusercontent.com/u/225513" 286 | }, 287 | { 288 | "username": "claesg", 289 | "href": "https://github.com/claesg", 290 | "avatar": "https://avatars2.githubusercontent.com/u/13845590" 291 | }, 292 | { 293 | "username": "girtsf", 294 | "href": "https://github.com/girtsf", 295 | "avatar": "https://avatars0.githubusercontent.com/u/260191" 296 | }, 297 | { 298 | "username": "gitter-badger", 299 | "href": "https://github.com/gitter-badger", 300 | "avatar": "https://avatars3.githubusercontent.com/u/8518239" 301 | } 302 | ] 303 | }, 304 | { 305 | "author": "google-research", 306 | "name": "electra", 307 | "avatar": "https://github.com/google-research.png", 308 | "url": "https://github.com/google-research/electra", 309 | "description": "ELECTRA: Pre-training Text Encoders as Discriminators Rather Than Generators", 310 | "language": "Python", 311 | "languageColor": "#3572A5", 312 | "stars": 355, 313 | "forks": 31, 314 | "currentPeriodStars": 96, 315 | "builtBy": [ 316 | { 317 | "username": "clarkkev", 318 | "href": "https://github.com/clarkkev", 319 | "avatar": "https://avatars3.githubusercontent.com/u/1091306" 320 | }, 321 | { 322 | "username": "stefan-it", 323 | "href": "https://github.com/stefan-it", 324 | "avatar": "https://avatars0.githubusercontent.com/u/20651387" 325 | }, 326 | { 327 | "username": "michelole", 328 | "href": "https://github.com/michelole", 329 | "avatar": "https://avatars1.githubusercontent.com/u/1688126" 330 | } 331 | ] 332 | }, 333 | { 334 | "author": "doocs", 335 | "name": "advanced-java", 336 | "avatar": "https://github.com/doocs.png", 337 | "url": "https://github.com/doocs/advanced-java", 338 | "description": "😮 互联网 Java 工程师进阶知识完全扫盲:涵盖高并发、分布式、高可用、微服务、海量数据处理等领域知识,后端同学必看,前端同学也可学习", 339 | "language": "Java", 340 | "languageColor": "#b07219", 341 | "stars": 40079, 342 | "forks": 11039, 343 | "currentPeriodStars": 133, 344 | "builtBy": [ 345 | { 346 | "username": "yanglbme", 347 | "href": "https://github.com/yanglbme", 348 | "avatar": "https://avatars3.githubusercontent.com/u/21008209" 349 | }, 350 | { 351 | "username": "ImgBotApp", 352 | "href": "https://github.com/ImgBotApp", 353 | "avatar": "https://avatars2.githubusercontent.com/u/31427850" 354 | }, 355 | { 356 | "username": "lihangqi", 357 | "href": "https://github.com/lihangqi", 358 | "avatar": "https://avatars2.githubusercontent.com/u/26339055" 359 | }, 360 | { 361 | "username": "huifer", 362 | "href": "https://github.com/huifer", 363 | "avatar": "https://avatars0.githubusercontent.com/u/26766909" 364 | }, 365 | { 366 | "username": "chenqimiao", 367 | "href": "https://github.com/chenqimiao", 368 | "avatar": "https://avatars1.githubusercontent.com/u/15151483" 369 | } 370 | ] 371 | }, 372 | { 373 | "author": "pytorch", 374 | "name": "pytorch", 375 | "avatar": "https://github.com/pytorch.png", 376 | "url": "https://github.com/pytorch/pytorch", 377 | "description": "Tensors and Dynamic neural networks in Python with strong GPU acceleration", 378 | "language": "C++", 379 | "languageColor": "#f34b7d", 380 | "stars": 36846, 381 | "forks": 9358, 382 | "currentPeriodStars": 55, 383 | "builtBy": [ 384 | { 385 | "username": "ezyang", 386 | "href": "https://github.com/ezyang", 387 | "avatar": "https://avatars0.githubusercontent.com/u/13564" 388 | }, 389 | { 390 | "username": "soumith", 391 | "href": "https://github.com/soumith", 392 | "avatar": "https://avatars1.githubusercontent.com/u/1310570" 393 | }, 394 | { 395 | "username": "gchanan", 396 | "href": "https://github.com/gchanan", 397 | "avatar": "https://avatars1.githubusercontent.com/u/3768583" 398 | }, 399 | { 400 | "username": "apaszke", 401 | "href": "https://github.com/apaszke", 402 | "avatar": "https://avatars0.githubusercontent.com/u/4583066" 403 | }, 404 | { 405 | "username": "Yangqing", 406 | "href": "https://github.com/Yangqing", 407 | "avatar": "https://avatars3.githubusercontent.com/u/551151" 408 | } 409 | ] 410 | }, 411 | { 412 | "author": "EvgenyKashin", 413 | "name": "stylegan2-distillation", 414 | "avatar": "https://github.com/EvgenyKashin.png", 415 | "url": "https://github.com/EvgenyKashin/stylegan2-distillation", 416 | "description": "", 417 | "stars": 273, 418 | "forks": 18, 419 | "currentPeriodStars": 88, 420 | "builtBy": [ 421 | { 422 | "username": "EvgenyKashin", 423 | "href": "https://github.com/EvgenyKashin", 424 | "avatar": "https://avatars2.githubusercontent.com/u/21174773" 425 | } 426 | ] 427 | }, 428 | { 429 | "author": "mai-lang-chai", 430 | "name": "Middleware-Vulnerability-detection", 431 | "avatar": "https://github.com/mai-lang-chai.png", 432 | "url": "https://github.com/mai-lang-chai/Middleware-Vulnerability-detection", 433 | "description": "CVE、CMS、中间件漏洞检测利用合集 Since 2019-9-15", 434 | "language": "Python", 435 | "languageColor": "#3572A5", 436 | "stars": 349, 437 | "forks": 81, 438 | "currentPeriodStars": 47, 439 | "builtBy": [ 440 | { 441 | "username": "mai-lang-chai", 442 | "href": "https://github.com/mai-lang-chai", 443 | "avatar": "https://avatars1.githubusercontent.com/u/36095584" 444 | } 445 | ] 446 | }, 447 | { 448 | "author": "MhdHejazi", 449 | "name": "CoronaTracker", 450 | "avatar": "https://github.com/MhdHejazi.png", 451 | "url": "https://github.com/MhdHejazi/CoronaTracker", 452 | "description": "Coronavirus tracker app for iOS & macOS with map & charts", 453 | "language": "Swift", 454 | "languageColor": "#ffac45", 455 | "stars": 207, 456 | "forks": 22, 457 | "currentPeriodStars": 57, 458 | "builtBy": [ 459 | { 460 | "username": "MhdHejazi", 461 | "href": "https://github.com/MhdHejazi", 462 | "avatar": "https://avatars1.githubusercontent.com/u/121827" 463 | } 464 | ] 465 | }, 466 | { 467 | "author": "taviso", 468 | "name": "avscript", 469 | "avatar": "https://github.com/taviso.png", 470 | "url": "https://github.com/taviso/avscript", 471 | "description": "Avast JavaScript Interactive Shell", 472 | "language": "C", 473 | "languageColor": "#555555", 474 | "stars": 485, 475 | "forks": 32, 476 | "currentPeriodStars": 171, 477 | "builtBy": [ 478 | { 479 | "username": "taviso", 480 | "href": "https://github.com/taviso", 481 | "avatar": "https://avatars0.githubusercontent.com/u/123814" 482 | }, 483 | { 484 | "username": "Vipul97", 485 | "href": "https://github.com/Vipul97", 486 | "avatar": "https://avatars2.githubusercontent.com/u/16150834" 487 | } 488 | ] 489 | }, 490 | { 491 | "author": "donnemartin", 492 | "name": "system-design-primer", 493 | "avatar": "https://github.com/donnemartin.png", 494 | "url": "https://github.com/donnemartin/system-design-primer", 495 | "description": "Learn how to design large-scale systems. Prep for the system design interview. Includes Anki flashcards.", 496 | "language": "Python", 497 | "languageColor": "#3572A5", 498 | "stars": 84966, 499 | "forks": 14435, 500 | "currentPeriodStars": 148, 501 | "builtBy": [ 502 | { 503 | "username": "donnemartin", 504 | "href": "https://github.com/donnemartin", 505 | "avatar": "https://avatars0.githubusercontent.com/u/5458997" 506 | }, 507 | { 508 | "username": "satob", 509 | "href": "https://github.com/satob", 510 | "avatar": "https://avatars0.githubusercontent.com/u/171818" 511 | }, 512 | { 513 | "username": "fluency03", 514 | "href": "https://github.com/fluency03", 515 | "avatar": "https://avatars3.githubusercontent.com/u/7440735" 516 | }, 517 | { 518 | "username": "antongulikov", 519 | "href": "https://github.com/antongulikov", 520 | "avatar": "https://avatars1.githubusercontent.com/u/6084440" 521 | }, 522 | { 523 | "username": "fabriziocucci", 524 | "href": "https://github.com/fabriziocucci", 525 | "avatar": "https://avatars0.githubusercontent.com/u/8156463" 526 | } 527 | ] 528 | }, 529 | { 530 | "author": "CTF-MissFeng", 531 | "name": "bayonet", 532 | "avatar": "https://github.com/CTF-MissFeng.png", 533 | "url": "https://github.com/CTF-MissFeng/bayonet", 534 | "description": "bayonet是一款src资产管理系统,从子域名、端口服务、漏洞、爬虫等一体化的资产管理系统", 535 | "language": "Python", 536 | "languageColor": "#3572A5", 537 | "stars": 272, 538 | "forks": 42, 539 | "currentPeriodStars": 48, 540 | "builtBy": [ 541 | { 542 | "username": "CTF-MissFeng", 543 | "href": "https://github.com/CTF-MissFeng", 544 | "avatar": "https://avatars0.githubusercontent.com/u/38177965" 545 | } 546 | ] 547 | }, 548 | { 549 | "author": "PizzaPokerGuy", 550 | "name": "ultimate-coding-resources", 551 | "avatar": "https://github.com/PizzaPokerGuy.png", 552 | "url": "https://github.com/PizzaPokerGuy/ultimate-coding-resources", 553 | "description": "A collection of the best resources for programming, web development, computer science and more.", 554 | "stars": 1475, 555 | "forks": 146, 556 | "currentPeriodStars": 235, 557 | "builtBy": [ 558 | { 559 | "username": "PizzaPokerGuy", 560 | "href": "https://github.com/PizzaPokerGuy", 561 | "avatar": "https://avatars0.githubusercontent.com/u/14266817" 562 | }, 563 | { 564 | "username": "samedwards1989", 565 | "href": "https://github.com/samedwards1989", 566 | "avatar": "https://avatars3.githubusercontent.com/u/11388164" 567 | }, 568 | { 569 | "username": "ahavensdaxko", 570 | "href": "https://github.com/ahavensdaxko", 571 | "avatar": "https://avatars0.githubusercontent.com/u/33287549" 572 | }, 573 | { 574 | "username": "krishnadevz", 575 | "href": "https://github.com/krishnadevz", 576 | "avatar": "https://avatars2.githubusercontent.com/u/42638797" 577 | } 578 | ] 579 | }, 580 | { 581 | "author": "Tencent", 582 | "name": "DVQA", 583 | "avatar": "https://github.com/Tencent.png", 584 | "url": "https://github.com/Tencent/DVQA", 585 | "description": "Deep learning-based Video Quality Assessment", 586 | "language": "Python", 587 | "languageColor": "#3572A5", 588 | "stars": 68, 589 | "forks": 14, 590 | "currentPeriodStars": 22, 591 | "builtBy": [ 592 | { 593 | "username": "tommyhq", 594 | "href": "https://github.com/tommyhq", 595 | "avatar": "https://avatars1.githubusercontent.com/u/61727295" 596 | } 597 | ] 598 | }, 599 | { 600 | "author": "emergenzeHack", 601 | "name": "covid19italia", 602 | "avatar": "https://github.com/emergenzeHack.png", 603 | "url": "https://github.com/emergenzeHack/covid19italia", 604 | "description": "Condividiamo informazioni e segnalazioni sul COVID19", 605 | "language": "JavaScript", 606 | "languageColor": "#f1e05a", 607 | "stars": 25, 608 | "forks": 17, 609 | "currentPeriodStars": 8, 610 | "builtBy": [ 611 | { 612 | "username": "mfortini", 613 | "href": "https://github.com/mfortini", 614 | "avatar": "https://avatars0.githubusercontent.com/u/1104926" 615 | }, 616 | { 617 | "username": "iltempe", 618 | "href": "https://github.com/iltempe", 619 | "avatar": "https://avatars3.githubusercontent.com/u/6368214" 620 | }, 621 | { 622 | "username": "avivace", 623 | "href": "https://github.com/avivace", 624 | "avatar": "https://avatars2.githubusercontent.com/u/14352721" 625 | }, 626 | { 627 | "username": "tailot", 628 | "href": "https://github.com/tailot", 629 | "avatar": "https://avatars0.githubusercontent.com/u/40148896" 630 | }, 631 | { 632 | "username": "olistik", 633 | "href": "https://github.com/olistik", 634 | "avatar": "https://avatars0.githubusercontent.com/u/21038" 635 | } 636 | ] 637 | }, 638 | { 639 | "author": "iluwatar", 640 | "name": "java-design-patterns", 641 | "avatar": "https://github.com/iluwatar.png", 642 | "url": "https://github.com/iluwatar/java-design-patterns", 643 | "description": "Design patterns implemented in Java", 644 | "language": "Java", 645 | "languageColor": "#b07219", 646 | "stars": 56063, 647 | "forks": 18039, 648 | "currentPeriodStars": 85, 649 | "builtBy": [ 650 | { 651 | "username": "iluwatar", 652 | "href": "https://github.com/iluwatar", 653 | "avatar": "https://avatars0.githubusercontent.com/u/582346" 654 | }, 655 | { 656 | "username": "npathai", 657 | "href": "https://github.com/npathai", 658 | "avatar": "https://avatars0.githubusercontent.com/u/1792515" 659 | }, 660 | { 661 | "username": "mikulucky", 662 | "href": "https://github.com/mikulucky", 663 | "avatar": "https://avatars2.githubusercontent.com/u/4526195" 664 | }, 665 | { 666 | "username": "fluxw42", 667 | "href": "https://github.com/fluxw42", 668 | "avatar": "https://avatars2.githubusercontent.com/u/1545460" 669 | }, 670 | { 671 | "username": "thomasoss", 672 | "href": "https://github.com/thomasoss", 673 | "avatar": "https://avatars2.githubusercontent.com/u/22516154" 674 | } 675 | ] 676 | }, 677 | { 678 | "author": "lemire", 679 | "name": "fast_double_parser", 680 | "avatar": "https://github.com/lemire.png", 681 | "url": "https://github.com/lemire/fast_double_parser", 682 | "description": "Fast function to parse strings into double (binary64) floating-point values", 683 | "language": "C++", 684 | "languageColor": "#f34b7d", 685 | "stars": 221, 686 | "forks": 9, 687 | "currentPeriodStars": 70, 688 | "builtBy": [ 689 | { 690 | "username": "lemire", 691 | "href": "https://github.com/lemire", 692 | "avatar": "https://avatars2.githubusercontent.com/u/391987" 693 | }, 694 | { 695 | "username": "facontidavide", 696 | "href": "https://github.com/facontidavide", 697 | "avatar": "https://avatars1.githubusercontent.com/u/2822888" 698 | } 699 | ] 700 | }, 701 | { 702 | "author": "blaCCkHatHacEEkr", 703 | "name": "PENTESTING-BIBLE", 704 | "avatar": "https://github.com/blaCCkHatHacEEkr.png", 705 | "url": "https://github.com/blaCCkHatHacEEkr/PENTESTING-BIBLE", 706 | "description": "This repository was created and developed by Ammar Amer @cry__pto Only. Updates to this repository will continue to arrive until the number of links reaches 10000 links & 10000 pdf files .Learn Ethical Hacking and penetration testing .hundreds of ethical hacking & penetration testing & red team & cyber security & computer science resources.", 707 | "stars": 4175, 708 | "forks": 821, 709 | "currentPeriodStars": 23, 710 | "builtBy": [ 711 | { 712 | "username": "blaCCkHatHacEEkr", 713 | "href": "https://github.com/blaCCkHatHacEEkr", 714 | "avatar": "https://avatars0.githubusercontent.com/u/51203763" 715 | }, 716 | { 717 | "username": "erjanmx", 718 | "href": "https://github.com/erjanmx", 719 | "avatar": "https://avatars3.githubusercontent.com/u/4899432" 720 | } 721 | ] 722 | }, 723 | { 724 | "author": "livewire", 725 | "name": "livewire", 726 | "avatar": "https://github.com/livewire.png", 727 | "url": "https://github.com/livewire/livewire", 728 | "description": "A full-stack framework for Laravel that takes the pain out of building dynamic UIs.", 729 | "language": "PHP", 730 | "languageColor": "#4F5D95", 731 | "stars": 2354, 732 | "forks": 172, 733 | "currentPeriodStars": 36, 734 | "builtBy": [ 735 | { 736 | "username": "calebporzio", 737 | "href": "https://github.com/calebporzio", 738 | "avatar": "https://avatars3.githubusercontent.com/u/3670578" 739 | }, 740 | { 741 | "username": "lancepioch", 742 | "href": "https://github.com/lancepioch", 743 | "avatar": "https://avatars3.githubusercontent.com/u/1296882" 744 | }, 745 | { 746 | "username": "tillkruss", 747 | "href": "https://github.com/tillkruss", 748 | "avatar": "https://avatars3.githubusercontent.com/u/665029" 749 | }, 750 | { 751 | "username": "nuernbergerA", 752 | "href": "https://github.com/nuernbergerA", 753 | "avatar": "https://avatars2.githubusercontent.com/u/13331388" 754 | } 755 | ] 756 | } 757 | ] 758 | -------------------------------------------------------------------------------- /cypress/fixtures/trending.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "author": "pcm-dpc", 4 | "name": "COVID-19", 5 | "avatar": "https://github.com/pcm-dpc.png", 6 | "url": "https://github.com/pcm-dpc/COVID-19", 7 | "description": "COVID-19 Italia - Monitoraggio situazione", 8 | "stars": 774, 9 | "forks": 144, 10 | "currentPeriodStars": 286, 11 | "builtBy": [ 12 | { 13 | "username": "brucellino", 14 | "href": "https://github.com/brucellino", 15 | "avatar": "https://avatars0.githubusercontent.com/u/2115428" 16 | }, 17 | { 18 | "username": "umbros", 19 | "href": "https://github.com/umbros", 20 | "avatar": "https://avatars1.githubusercontent.com/u/4085151" 21 | } 22 | ] 23 | }, 24 | { 25 | "author": "ouyanghuiyu", 26 | "name": "chineseocr_lite", 27 | "avatar": "https://github.com/ouyanghuiyu.png", 28 | "url": "https://github.com/ouyanghuiyu/chineseocr_lite", 29 | "description": "超轻量级中文ocr,支持竖排文字识别, 支持ncnn推理 , psenet(8.5M) + crnn(6.3M) + anglenet(1.5M) 总模型仅17M", 30 | "language": "C++", 31 | "languageColor": "#f34b7d", 32 | "stars": 1753, 33 | "forks": 215, 34 | "currentPeriodStars": 400, 35 | "builtBy": [ 36 | { 37 | "username": "ouyanghuiyu", 38 | "href": "https://github.com/ouyanghuiyu", 39 | "avatar": "https://avatars3.githubusercontent.com/u/42023607" 40 | } 41 | ] 42 | }, 43 | { 44 | "author": "leelovejava", 45 | "name": "cloud2020", 46 | "avatar": "https://github.com/leelovejava.png", 47 | "url": "https://github.com/leelovejava/cloud2020", 48 | "description": "SpringCloud", 49 | "language": "Java", 50 | "languageColor": "#b07219", 51 | "stars": 76, 52 | "forks": 59, 53 | "currentPeriodStars": 12, 54 | "builtBy": [ 55 | { 56 | "username": "leelovejava", 57 | "href": "https://github.com/leelovejava", 58 | "avatar": "https://avatars2.githubusercontent.com/u/20348936" 59 | } 60 | ] 61 | }, 62 | { 63 | "author": "AobingJava", 64 | "name": "JavaFamily", 65 | "avatar": "https://github.com/AobingJava.png", 66 | "url": "https://github.com/AobingJava/JavaFamily", 67 | "description": "【互联网一线大厂面试+学习指南】进阶知识完全扫盲:涵盖高并发、分布式、高可用、微服务等领域知识,作者风格幽默,看起来津津有味,把学习当做一种乐趣,何乐而不为,后端同学必看,前端同学我保证你也看得懂,看不懂你加我微信骂我渣男就好了。", 68 | "stars": 7740, 69 | "forks": 1384, 70 | "currentPeriodStars": 143, 71 | "builtBy": [ 72 | { 73 | "username": "AobingJava", 74 | "href": "https://github.com/AobingJava", 75 | "avatar": "https://avatars3.githubusercontent.com/u/41898583" 76 | } 77 | ] 78 | }, 79 | { 80 | "author": "iikira", 81 | "name": "BaiduPCS-Go", 82 | "avatar": "https://github.com/iikira.png", 83 | "url": "https://github.com/iikira/BaiduPCS-Go", 84 | "description": "百度网盘客户端 - Go语言编写", 85 | "language": "Go", 86 | "languageColor": "#00ADD8", 87 | "stars": 21262, 88 | "forks": 3318, 89 | "currentPeriodStars": 169, 90 | "builtBy": [ 91 | { 92 | "username": "iikira", 93 | "href": "https://github.com/iikira", 94 | "avatar": "https://avatars1.githubusercontent.com/u/19154488" 95 | }, 96 | { 97 | "username": "apocelipes", 98 | "href": "https://github.com/apocelipes", 99 | "avatar": "https://avatars3.githubusercontent.com/u/21255940" 100 | }, 101 | { 102 | "username": "daobee", 103 | "href": "https://github.com/daobee", 104 | "avatar": "https://avatars1.githubusercontent.com/u/21331325" 105 | }, 106 | { 107 | "username": "hianghokung", 108 | "href": "https://github.com/hianghokung", 109 | "avatar": "https://avatars2.githubusercontent.com/u/22436288" 110 | }, 111 | { 112 | "username": "88250", 113 | "href": "https://github.com/88250", 114 | "avatar": "https://avatars1.githubusercontent.com/u/873584" 115 | } 116 | ] 117 | }, 118 | { 119 | "author": "Snailclimb", 120 | "name": "JavaGuide", 121 | "avatar": "https://github.com/Snailclimb.png", 122 | "url": "https://github.com/Snailclimb/JavaGuide", 123 | "description": "【Java学习+面试指南】 一份涵盖大部分Java程序员所需要掌握的核心知识。", 124 | "language": "Java", 125 | "languageColor": "#b07219", 126 | "stars": 71014, 127 | "forks": 24346, 128 | "currentPeriodStars": 219, 129 | "builtBy": [ 130 | { 131 | "username": "Snailclimb", 132 | "href": "https://github.com/Snailclimb", 133 | "avatar": "https://avatars0.githubusercontent.com/u/29880145" 134 | }, 135 | { 136 | "username": "Goose9527", 137 | "href": "https://github.com/Goose9527", 138 | "avatar": "https://avatars0.githubusercontent.com/u/43314997" 139 | }, 140 | { 141 | "username": "LiWenGu", 142 | "href": "https://github.com/LiWenGu", 143 | "avatar": "https://avatars0.githubusercontent.com/u/15909210" 144 | }, 145 | { 146 | "username": "Ryze-Zhao", 147 | "href": "https://github.com/Ryze-Zhao", 148 | "avatar": "https://avatars1.githubusercontent.com/u/38486503" 149 | }, 150 | { 151 | "username": "fanofxiaofeng", 152 | "href": "https://github.com/fanofxiaofeng", 153 | "avatar": "https://avatars3.githubusercontent.com/u/3983683" 154 | } 155 | ] 156 | }, 157 | { 158 | "author": "PizzaPokerGuy", 159 | "name": "ultimate-coding-resources", 160 | "avatar": "https://github.com/PizzaPokerGuy.png", 161 | "url": "https://github.com/PizzaPokerGuy/ultimate-coding-resources", 162 | "description": "A collection of the best resources for programming, web development, computer science and more.", 163 | "stars": 944, 164 | "forks": 97, 165 | "currentPeriodStars": 469, 166 | "builtBy": [ 167 | { 168 | "username": "PizzaPokerGuy", 169 | "href": "https://github.com/PizzaPokerGuy", 170 | "avatar": "https://avatars0.githubusercontent.com/u/14266817" 171 | }, 172 | { 173 | "username": "samedwards1989", 174 | "href": "https://github.com/samedwards1989", 175 | "avatar": "https://avatars3.githubusercontent.com/u/11388164" 176 | }, 177 | { 178 | "username": "ahavensdaxko", 179 | "href": "https://github.com/ahavensdaxko", 180 | "avatar": "https://avatars0.githubusercontent.com/u/33287549" 181 | } 182 | ] 183 | }, 184 | { 185 | "author": "CSSEGISandData", 186 | "name": "COVID-19", 187 | "avatar": "https://github.com/CSSEGISandData.png", 188 | "url": "https://github.com/CSSEGISandData/COVID-19", 189 | "description": "Novel Coronavirus (COVID-19) Cases, provided by JHU CSSE", 190 | "stars": 5609, 191 | "forks": 1908, 192 | "currentPeriodStars": 767, 193 | "builtBy": [ 194 | { 195 | "username": "CSSEGISandData", 196 | "href": "https://github.com/CSSEGISandData", 197 | "avatar": "https://avatars3.githubusercontent.com/u/60674295" 198 | }, 199 | { 200 | "username": "hongru94", 201 | "href": "https://github.com/hongru94", 202 | "avatar": "https://avatars3.githubusercontent.com/u/47940478" 203 | }, 204 | { 205 | "username": "enshengdong", 206 | "href": "https://github.com/enshengdong", 207 | "avatar": "https://avatars0.githubusercontent.com/u/10015024" 208 | } 209 | ] 210 | }, 211 | { 212 | "author": "amusi", 213 | "name": "CVPR2020-Code", 214 | "avatar": "https://github.com/amusi.png", 215 | "url": "https://github.com/amusi/CVPR2020-Code", 216 | "description": "CVPR 2020 论文开源项目合集", 217 | "stars": 444, 218 | "forks": 61, 219 | "currentPeriodStars": 130, 220 | "builtBy": [ 221 | { 222 | "username": "amusi", 223 | "href": "https://github.com/amusi", 224 | "avatar": "https://avatars2.githubusercontent.com/u/22436957" 225 | } 226 | ] 227 | }, 228 | { 229 | "author": "subnub", 230 | "name": "myDrive", 231 | "avatar": "https://github.com/subnub.png", 232 | "url": "https://github.com/subnub/myDrive", 233 | "description": "Node.js and mongoDB Google Drive Clone", 234 | "language": "JavaScript", 235 | "languageColor": "#f1e05a", 236 | "stars": 662, 237 | "forks": 78, 238 | "currentPeriodStars": 279, 239 | "builtBy": [ 240 | { 241 | "username": "subnub", 242 | "href": "https://github.com/subnub", 243 | "avatar": "https://avatars3.githubusercontent.com/u/44621867" 244 | }, 245 | { 246 | "username": "ImMaax", 247 | "href": "https://github.com/ImMaax", 248 | "avatar": "https://avatars2.githubusercontent.com/u/40642083" 249 | } 250 | ] 251 | }, 252 | { 253 | "author": "ziishaned", 254 | "name": "learn-regex", 255 | "avatar": "https://github.com/ziishaned.png", 256 | "url": "https://github.com/ziishaned/learn-regex", 257 | "description": "Learn regex the easy way", 258 | "stars": 33842, 259 | "forks": 4628, 260 | "currentPeriodStars": 208, 261 | "builtBy": [ 262 | { 263 | "username": "ziishaned", 264 | "href": "https://github.com/ziishaned", 265 | "avatar": "https://avatars2.githubusercontent.com/u/16267321" 266 | }, 267 | { 268 | "username": "munkacsimark", 269 | "href": "https://github.com/munkacsimark", 270 | "avatar": "https://avatars2.githubusercontent.com/u/7120785" 271 | }, 272 | { 273 | "username": "hjavadish", 274 | "href": "https://github.com/hjavadish", 275 | "avatar": "https://avatars2.githubusercontent.com/u/8628797" 276 | }, 277 | { 278 | "username": "wicksome", 279 | "href": "https://github.com/wicksome", 280 | "avatar": "https://avatars2.githubusercontent.com/u/5036939" 281 | }, 282 | { 283 | "username": "ImKifu", 284 | "href": "https://github.com/ImKifu", 285 | "avatar": "https://avatars2.githubusercontent.com/u/37338905" 286 | } 287 | ] 288 | }, 289 | { 290 | "author": "Screetsec", 291 | "name": "TheFatRat", 292 | "avatar": "https://github.com/Screetsec.png", 293 | "url": "https://github.com/Screetsec/TheFatRat", 294 | "description": "Thefatrat a massive exploiting tool : Easy tool to generate backdoor and easy tool to post exploitation attack like browser attack and etc . This tool compiles a malware with popular payload and then the compiled malware can be execute on windows, android, mac . The malware that created with this tool also have an ability to bypass most AV softw…", 295 | "language": "C", 296 | "languageColor": "#555555", 297 | "stars": 3850, 298 | "forks": 1340, 299 | "currentPeriodStars": 64, 300 | "builtBy": [ 301 | { 302 | "username": "Screetsec", 303 | "href": "https://github.com/Screetsec", 304 | "avatar": "https://avatars3.githubusercontent.com/u/17976841" 305 | }, 306 | { 307 | "username": "peterpt", 308 | "href": "https://github.com/peterpt", 309 | "avatar": "https://avatars3.githubusercontent.com/u/7487321" 310 | }, 311 | { 312 | "username": "mrusme", 313 | "href": "https://github.com/mrusme", 314 | "avatar": "https://avatars0.githubusercontent.com/u/151967" 315 | }, 316 | { 317 | "username": "n0login", 318 | "href": "https://github.com/n0login", 319 | "avatar": "https://avatars2.githubusercontent.com/u/21091592" 320 | }, 321 | { 322 | "username": "isfaaghyth", 323 | "href": "https://github.com/isfaaghyth", 324 | "avatar": "https://avatars2.githubusercontent.com/u/6775159" 325 | } 326 | ] 327 | }, 328 | { 329 | "author": "guidovranken", 330 | "name": "vfuzz", 331 | "avatar": "https://github.com/guidovranken.png", 332 | "url": "https://github.com/guidovranken/vfuzz", 333 | "description": "vfuzz", 334 | "language": "C++", 335 | "languageColor": "#f34b7d", 336 | "stars": 119, 337 | "forks": 22, 338 | "currentPeriodStars": 44, 339 | "builtBy": [ 340 | { 341 | "username": "guidovranken", 342 | "href": "https://github.com/guidovranken", 343 | "avatar": "https://avatars1.githubusercontent.com/u/6846644" 344 | } 345 | ] 346 | }, 347 | { 348 | "author": "firstlookmedia", 349 | "name": "dangerzone", 350 | "avatar": "https://github.com/firstlookmedia.png", 351 | "url": "https://github.com/firstlookmedia/dangerzone", 352 | "description": "Take potentially dangerous PDFs, office documents, or images and convert them to a safe PDF", 353 | "language": "Python", 354 | "languageColor": "#3572A5", 355 | "stars": 626, 356 | "forks": 20, 357 | "currentPeriodStars": 135, 358 | "builtBy": [ 359 | { 360 | "username": "micahflee", 361 | "href": "https://github.com/micahflee", 362 | "avatar": "https://avatars1.githubusercontent.com/u/156128" 363 | } 364 | ] 365 | }, 366 | { 367 | "author": "ajeetdsouza", 368 | "name": "zoxide", 369 | "avatar": "https://github.com/ajeetdsouza.png", 370 | "url": "https://github.com/ajeetdsouza/zoxide", 371 | "description": "A fast cd command that learns your habits", 372 | "language": "Rust", 373 | "languageColor": "#dea584", 374 | "stars": 353, 375 | "forks": 11, 376 | "currentPeriodStars": 93, 377 | "builtBy": [ 378 | { 379 | "username": "ajeetdsouza", 380 | "href": "https://github.com/ajeetdsouza", 381 | "avatar": "https://avatars3.githubusercontent.com/u/1777663" 382 | }, 383 | { 384 | "username": "alin23", 385 | "href": "https://github.com/alin23", 386 | "avatar": "https://avatars1.githubusercontent.com/u/3819725" 387 | }, 388 | { 389 | "username": "crazystylus", 390 | "href": "https://github.com/crazystylus", 391 | "avatar": "https://avatars1.githubusercontent.com/u/29002863" 392 | }, 393 | { 394 | "username": "ErichDonGubler", 395 | "href": "https://github.com/ErichDonGubler", 396 | "avatar": "https://avatars0.githubusercontent.com/u/658538" 397 | }, 398 | { 399 | "username": "cust0dian", 400 | "href": "https://github.com/cust0dian", 401 | "avatar": "https://avatars0.githubusercontent.com/u/389387" 402 | } 403 | ] 404 | }, 405 | { 406 | "author": "six2dez", 407 | "name": "wahh_extras", 408 | "avatar": "https://github.com/six2dez.png", 409 | "url": "https://github.com/six2dez/wahh_extras", 410 | "description": "The Web Application Hacker's Handbook - Extra Content", 411 | "language": "Java", 412 | "languageColor": "#b07219", 413 | "stars": 262, 414 | "forks": 41, 415 | "currentPeriodStars": 75, 416 | "builtBy": [ 417 | { 418 | "username": "six2dez", 419 | "href": "https://github.com/six2dez", 420 | "avatar": "https://avatars1.githubusercontent.com/u/24670991" 421 | } 422 | ] 423 | }, 424 | { 425 | "author": "KeenS", 426 | "name": "webml", 427 | "avatar": "https://github.com/KeenS.png", 428 | "url": "https://github.com/KeenS/webml", 429 | "description": "A Standard ML Compiler for the Web", 430 | "language": "Rust", 431 | "languageColor": "#dea584", 432 | "stars": 212, 433 | "forks": 7, 434 | "currentPeriodStars": 31, 435 | "builtBy": [ 436 | { 437 | "username": "KeenS", 438 | "href": "https://github.com/KeenS", 439 | "avatar": "https://avatars2.githubusercontent.com/u/4434568" 440 | } 441 | ] 442 | }, 443 | { 444 | "author": "marcinguy", 445 | "name": "CVE-2020-8597", 446 | "avatar": "https://github.com/marcinguy.png", 447 | "url": "https://github.com/marcinguy/CVE-2020-8597", 448 | "description": "CVE-2020-8597", 449 | "language": "Python", 450 | "languageColor": "#3572A5", 451 | "stars": 38, 452 | "forks": 15, 453 | "currentPeriodStars": 8, 454 | "builtBy": [ 455 | { 456 | "username": "marcinguy", 457 | "href": "https://github.com/marcinguy", 458 | "avatar": "https://avatars2.githubusercontent.com/u/20355405" 459 | } 460 | ] 461 | }, 462 | { 463 | "author": "Netflix", 464 | "name": "dispatch", 465 | "avatar": "https://github.com/Netflix.png", 466 | "url": "https://github.com/Netflix/dispatch", 467 | "description": "All of the ad-hoc things you're doing to manage incidents today, done for you, and much more!", 468 | "language": "Python", 469 | "languageColor": "#3572A5", 470 | "stars": 1644, 471 | "forks": 90, 472 | "currentPeriodStars": 62, 473 | "builtBy": [ 474 | { 475 | "username": "kevgliss", 476 | "href": "https://github.com/kevgliss", 477 | "avatar": "https://avatars2.githubusercontent.com/u/2262214" 478 | }, 479 | { 480 | "username": "mvilanova", 481 | "href": "https://github.com/mvilanova", 482 | "avatar": "https://avatars0.githubusercontent.com/u/39573146" 483 | }, 484 | { 485 | "username": "TheDahv", 486 | "href": "https://github.com/TheDahv", 487 | "avatar": "https://avatars3.githubusercontent.com/u/73363" 488 | }, 489 | { 490 | "username": "daniel-gallagher", 491 | "href": "https://github.com/daniel-gallagher", 492 | "avatar": "https://avatars2.githubusercontent.com/u/17089890" 493 | }, 494 | { 495 | "username": "abdullahselek", 496 | "href": "https://github.com/abdullahselek", 497 | "avatar": "https://avatars0.githubusercontent.com/u/5083377" 498 | } 499 | ] 500 | }, 501 | { 502 | "author": "apache", 503 | "name": "incubator-shardingsphere", 504 | "avatar": "https://github.com/apache.png", 505 | "url": "https://github.com/apache/incubator-shardingsphere", 506 | "description": "Distributed database middleware", 507 | "language": "Java", 508 | "languageColor": "#b07219", 509 | "stars": 10036, 510 | "forks": 3417, 511 | "currentPeriodStars": 44, 512 | "builtBy": [ 513 | { 514 | "username": "tristaZero", 515 | "href": "https://github.com/tristaZero", 516 | "avatar": "https://avatars2.githubusercontent.com/u/27757146" 517 | }, 518 | { 519 | "username": "terrymanu", 520 | "href": "https://github.com/terrymanu", 521 | "avatar": "https://avatars0.githubusercontent.com/u/5516298" 522 | }, 523 | { 524 | "username": "tuohai666", 525 | "href": "https://github.com/tuohai666", 526 | "avatar": "https://avatars2.githubusercontent.com/u/24643893" 527 | }, 528 | { 529 | "username": "cherrylzhao", 530 | "href": "https://github.com/cherrylzhao", 531 | "avatar": "https://avatars0.githubusercontent.com/u/8317649" 532 | }, 533 | { 534 | "username": "haocao", 535 | "href": "https://github.com/haocao", 536 | "avatar": "https://avatars0.githubusercontent.com/u/687732" 537 | } 538 | ] 539 | }, 540 | { 541 | "author": "dotnet", 542 | "name": "aspnetcore", 543 | "avatar": "https://github.com/dotnet.png", 544 | "url": "https://github.com/dotnet/aspnetcore", 545 | "description": "ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.", 546 | "language": "C#", 547 | "languageColor": "#178600", 548 | "stars": 16378, 549 | "forks": 4367, 550 | "currentPeriodStars": 41, 551 | "builtBy": [ 552 | { 553 | "username": "aspnetci", 554 | "href": "https://github.com/aspnetci", 555 | "avatar": "https://avatars3.githubusercontent.com/u/23037549" 556 | }, 557 | { 558 | "username": "pranavkm", 559 | "href": "https://github.com/pranavkm", 560 | "avatar": "https://avatars1.githubusercontent.com/u/174281" 561 | }, 562 | { 563 | "username": "SteveSandersonMS", 564 | "href": "https://github.com/SteveSandersonMS", 565 | "avatar": "https://avatars2.githubusercontent.com/u/1101362" 566 | }, 567 | { 568 | "username": "Tratcher", 569 | "href": "https://github.com/Tratcher", 570 | "avatar": "https://avatars1.githubusercontent.com/u/1821173" 571 | }, 572 | { 573 | "username": "rynowak", 574 | "href": "https://github.com/rynowak", 575 | "avatar": "https://avatars3.githubusercontent.com/u/1430011" 576 | } 577 | ] 578 | }, 579 | { 580 | "author": "HospitalRun", 581 | "name": "hospitalrun-frontend", 582 | "avatar": "https://github.com/HospitalRun.png", 583 | "url": "https://github.com/HospitalRun/hospitalrun-frontend", 584 | "description": "Frontend for HospitalRun", 585 | "language": "TypeScript", 586 | "languageColor": "#2b7489", 587 | "stars": 5080, 588 | "forks": 1555, 589 | "currentPeriodStars": 39, 590 | "builtBy": [ 591 | { 592 | "username": "jkleinsc", 593 | "href": "https://github.com/jkleinsc", 594 | "avatar": "https://avatars2.githubusercontent.com/u/609052" 595 | }, 596 | { 597 | "username": "tangollama", 598 | "href": "https://github.com/tangollama", 599 | "avatar": "https://avatars3.githubusercontent.com/u/929261" 600 | }, 601 | { 602 | "username": "jglovier", 603 | "href": "https://github.com/jglovier", 604 | "avatar": "https://avatars2.githubusercontent.com/u/1319791" 605 | }, 606 | { 607 | "username": "tehKapa", 608 | "href": "https://github.com/tehKapa", 609 | "avatar": "https://avatars0.githubusercontent.com/u/6388707" 610 | }, 611 | { 612 | "username": "hospitalrunbot", 613 | "href": "https://github.com/hospitalrunbot", 614 | "avatar": "https://avatars3.githubusercontent.com/u/22404737" 615 | } 616 | ] 617 | }, 618 | { 619 | "author": "CyC2018", 620 | "name": "CS-Notes", 621 | "avatar": "https://github.com/CyC2018.png", 622 | "url": "https://github.com/CyC2018/CS-Notes", 623 | "description": "📚 技术面试必备基础知识、Leetcode、计算机操作系统、计算机网络、系统设计、Java、Python、C++", 624 | "language": "Java", 625 | "languageColor": "#b07219", 626 | "stars": 93554, 627 | "forks": 30326, 628 | "currentPeriodStars": 280, 629 | "builtBy": [ 630 | { 631 | "username": "CyC2018", 632 | "href": "https://github.com/CyC2018", 633 | "avatar": "https://avatars1.githubusercontent.com/u/36260787" 634 | }, 635 | { 636 | "username": "kwongtailau", 637 | "href": "https://github.com/kwongtailau", 638 | "avatar": "https://avatars1.githubusercontent.com/u/22954582" 639 | }, 640 | { 641 | "username": "linehk", 642 | "href": "https://github.com/linehk", 643 | "avatar": "https://avatars0.githubusercontent.com/u/8375793" 644 | }, 645 | { 646 | "username": "crossoverJie", 647 | "href": "https://github.com/crossoverJie", 648 | "avatar": "https://avatars0.githubusercontent.com/u/15684156" 649 | }, 650 | { 651 | "username": "Lisanaaa", 652 | "href": "https://github.com/Lisanaaa", 653 | "avatar": "https://avatars1.githubusercontent.com/u/28261876" 654 | } 655 | ] 656 | }, 657 | { 658 | "author": "3b1b", 659 | "name": "manim", 660 | "avatar": "https://github.com/3b1b.png", 661 | "url": "https://github.com/3b1b/manim", 662 | "description": "Animation engine for explanatory math videos", 663 | "language": "Python", 664 | "languageColor": "#3572A5", 665 | "stars": 17548, 666 | "forks": 2108, 667 | "currentPeriodStars": 70, 668 | "builtBy": [ 669 | { 670 | "username": "3b1b", 671 | "href": "https://github.com/3b1b", 672 | "avatar": "https://avatars2.githubusercontent.com/u/11601040" 673 | }, 674 | { 675 | "username": "bhbr", 676 | "href": "https://github.com/bhbr", 677 | "avatar": "https://avatars2.githubusercontent.com/u/13440601" 678 | }, 679 | { 680 | "username": "eulertour", 681 | "href": "https://github.com/eulertour", 682 | "avatar": "https://avatars1.githubusercontent.com/u/43117506" 683 | }, 684 | { 685 | "username": "Sridhar3b1b", 686 | "href": "https://github.com/Sridhar3b1b", 687 | "avatar": "https://avatars1.githubusercontent.com/u/35234358" 688 | }, 689 | { 690 | "username": "mirefek", 691 | "href": "https://github.com/mirefek", 692 | "avatar": "https://avatars3.githubusercontent.com/u/25885450" 693 | } 694 | ] 695 | }, 696 | { 697 | "author": "UniversalDataTool", 698 | "name": "universal-data-tool", 699 | "avatar": "https://github.com/UniversalDataTool.png", 700 | "url": "https://github.com/UniversalDataTool/universal-data-tool", 701 | "description": "Collaborate & label any type of data, images, text, or documents, in an easy web interface or desktop app.", 702 | "language": "JavaScript", 703 | "languageColor": "#f1e05a", 704 | "stars": 281, 705 | "forks": 12, 706 | "currentPeriodStars": 41, 707 | "builtBy": [ 708 | { 709 | "username": "seveibar", 710 | "href": "https://github.com/seveibar", 711 | "avatar": "https://avatars3.githubusercontent.com/u/1910070" 712 | }, 713 | { 714 | "username": "puskuruk", 715 | "href": "https://github.com/puskuruk", 716 | "avatar": "https://avatars2.githubusercontent.com/u/22892227" 717 | } 718 | ] 719 | } 720 | ] 721 | -------------------------------------------------------------------------------- /cypress/integration/dark-mode.js: -------------------------------------------------------------------------------- 1 | describe('Dark Mode', () => { 2 | it('set local storage preferDarkMode to true if in dark mode', () => { 3 | cy.fetchReposAndWait({ darkMode: true }); 4 | cy.window().its('localStorage.preferDarkMode').should('eq', 'true'); 5 | cy.findByLabelText('Sun Icon').should('exist'); 6 | cy.findByLabelText('Moon Icon').should('not.exist'); 7 | }); 8 | 9 | it('set local storage preferDarkMode to false if in light mode', () => { 10 | cy.fetchReposAndWait({ darkMode: false }); 11 | cy.window().its('localStorage.preferDarkMode').should('eq', 'false'); 12 | cy.findByLabelText('Moon Icon').should('exist'); 13 | cy.findByLabelText('Sun Icon').should('not.exist'); 14 | }); 15 | 16 | it('click on icon should change mode and local storage value', () => { 17 | cy.fetchReposAndWait({ darkMode: true }); 18 | cy.findByLabelText('Sun Icon').click(); 19 | cy.findByLabelText('Moon Icon').should('exist'); 20 | cy.findByLabelText('Sun Icon').should('not.exist'); 21 | cy.getLocalStorage('preferDarkMode').should('eq', false); 22 | cy.findByLabelText('Moon Icon').click(); 23 | cy.findByLabelText('Moon Icon').should('not.exist'); 24 | cy.findByLabelText('Sun Icon').should('exist'); 25 | cy.getLocalStorage('preferDarkMode').should('eq', true); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /cypress/integration/empty-state.js: -------------------------------------------------------------------------------- 1 | describe('Empty state', () => { 2 | it('shows empty state when response is empty', () => { 3 | cy.fetchReposAndWait({ response: [] }); 4 | cy.findByText('Trending Repositories').should('not.exist'); 5 | cy.findByTestId('repo-card').should('not.exist'); 6 | cy.findByTestId('empty-state').should('exist'); 7 | }); 8 | 9 | it('does not show empty state when response is normal', () => { 10 | cy.fetchReposAndWait(); 11 | cy.findByText('empty-state').should('not.exist'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /cypress/integration/error-state.js: -------------------------------------------------------------------------------- 1 | describe('Error state', () => { 2 | it('shows error banner when response is not 200', () => { 3 | cy.errorFetchReposAndWait(); 4 | cy.findByTestId('empty-state').should('exist'); 5 | cy.findByTestId('network-error-banner').should('exist'); 6 | cy.findByLabelText('Close').click(); 7 | cy.findByTestId('network-error-banner').should('not.exist'); 8 | }); 9 | 10 | it('should reload when click retry', () => { 11 | cy.errorFetchReposAndWait(); 12 | cy.findByTestId('empty-state').should('exist'); 13 | cy.findByTestId('network-error-banner').should('exist'); 14 | cy.fetchRepos(); 15 | cy.findByText('Retry').click(); 16 | cy.findByTestId('network-error-banner').should('not.exist'); 17 | cy.shouldHaveRepoCards(25); 18 | }); 19 | 20 | it('should show error banner when repo was already loaded', () => { 21 | cy.fetchRepos({ status: 500 }); 22 | cy.seedLocalStorage(); 23 | cy.visit('/'); 24 | cy.findByTestId('last-updated-time').click(); 25 | cy.waitAllErrors(); 26 | cy.findByTestId('network-error-banner').should('exist'); 27 | cy.shouldHaveRepoCards(25); 28 | }); 29 | 30 | it('does not show error banner when response is normal', () => { 31 | cy.fetchReposAndWait(); 32 | cy.findByTestId('network-error-banner').should('not.exist'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /cypress/integration/feeling-lucky.js: -------------------------------------------------------------------------------- 1 | describe('I’m Feeling Lucky', () => { 2 | it('should have a lucky repo', () => { 3 | cy.fetchReposAndWait(); 4 | 5 | cy.fixture('trending').then((json) => { 6 | const names = json.map((repo) => repo.name); 7 | cy.findByText('I’m Feeling Lucky').should('exist'); 8 | cy.findByTestId('random-repo-list') 9 | .findAllByTestId('repo-card') 10 | .should('have.length', 1); 11 | cy.findByTestId('random-repo-list') 12 | .findByTestId('name') 13 | .invoke('text') 14 | .should('be.oneOf', names); 15 | cy.findByLabelText('Random Pick Button').click(); 16 | cy.findByTestId('random-repo-list') 17 | .findAllByTestId('repo-card') 18 | .should('have.length', 1); 19 | cy.findByTestId('random-repo-list') 20 | .findByTestId('name') 21 | .invoke('text') 22 | .should('be.oneOf', names); 23 | }); 24 | }); 25 | 26 | it('reload should update the picked item', () => { 27 | cy.fetchReposAndWait(); 28 | 29 | cy.fixture('trending').then((json) => { 30 | const names = json.map((repo) => repo.name); 31 | cy.findByTestId('random-repo-list') 32 | .findByTestId('name') 33 | .invoke('text') 34 | .should('be.oneOf', names); 35 | }); 36 | 37 | cy.fixture('trending-2').then((json) => { 38 | const names = json.map((repo) => repo.name); 39 | cy.route({ 40 | method: 'GET', 41 | url: 'https://ghapi.huchen.dev/repositories?since=daily', 42 | response: 'fixture:trending-2', 43 | }).as('fetchRepos'); 44 | cy.findByTestId('last-updated-time').click(); 45 | cy.waitResponse(); 46 | cy.findByTestId('random-repo-list') 47 | .findByTestId('name') 48 | .invoke('text') 49 | .should('be.oneOf', names); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /cypress/integration/load-repositories.js: -------------------------------------------------------------------------------- 1 | describe('Load Repositories', () => { 2 | it('load repo cards', () => { 3 | cy.fetchReposAndWait(); 4 | cy.findByText('Trending Repositories').should('exist'); 5 | cy.shouldHaveRepoCards(25); 6 | }); 7 | 8 | it('shows loading placeholder while loading', () => { 9 | cy.fetchRepos({ delay: 100 }); 10 | cy.visit('/'); 11 | cy.findAllByTestId('loading-card').should('have.length', 10); 12 | cy.wait('@fetchRepos'); 13 | cy.findByTestId('loading-card').should('not.exist'); 14 | cy.shouldHaveRepoCards(25); 15 | }); 16 | 17 | it('should set local storage values', () => { 18 | const now = new Date('2020-01-01T08:30:00').getTime(); 19 | cy.clock(now); 20 | cy.fetchReposAndWait(); 21 | cy.getLocalStorage('selectedLanguage').should('eq', '__ALL__'); 22 | cy.getLocalStorage('selectedPeriod').should('eq', 'daily'); 23 | cy.getLocalStorage('selectedSpokenLanguage').should('eq', '__ALL__'); 24 | cy.getLocalStorage('schemaVersion').should('eq', '2'); 25 | cy.getLocalStorage('lastUpdatedTime').should('eq', now); 26 | cy.fixture('trending').then((json) => { 27 | cy.getLocalStorage('repositories').should('deep.eq', json); 28 | }); 29 | }); 30 | 31 | it('should not fetch if repos is cached in local storage', () => { 32 | cy.server({ enable: false }); 33 | cy.setLocalStorage('schemaVersion', '2'); 34 | cy.fixture('trending').then((json) => { 35 | cy.setLocalStorage('repositories', json); 36 | }); 37 | cy.visit('/'); 38 | cy.findByTestId('loading-card').should('not.exist'); 39 | cy.shouldHaveRepoCards(25); 40 | cy.shouldHaveFirstCardContains('COVID-19 Italia - Monitoraggio situazione'); 41 | }); 42 | 43 | it('should show last updated time', () => { 44 | cy.seedLocalStorage(); 45 | cy.visit('/'); 46 | cy.findByText('10 minutes ago').should('exist'); 47 | }); 48 | 49 | it('click last updated time should refetch repositories', () => { 50 | const fixtureTime = new Date('2020-01-01T08:20:00').getTime(); 51 | cy.seedLocalStorage({ 52 | lastUpdatedTime: fixtureTime, 53 | }); 54 | cy.server(); 55 | cy.route({ 56 | method: 'GET', 57 | url: 'https://ghapi.huchen.dev/repositories?since=daily', 58 | response: 'fixture:trending-2', 59 | }).as('fetchRepos'); 60 | cy.visit('/'); 61 | cy.findByTestId('last-updated-time').click(); 62 | cy.waitResponse(); 63 | cy.getLocalStorage('lastUpdatedTime').should('be.greaterThan', fixtureTime); 64 | cy.fixture('trending-2').then((json) => { 65 | cy.getLocalStorage('repositories').should('deep.eq', json); 66 | }); 67 | cy.shouldHaveFirstCardContains( 68 | 'An operating system designed for hosting containers' 69 | ); 70 | }); 71 | 72 | it('clears localStorage if schema version is different', () => { 73 | const now = new Date('2020-01-01T08:30:00').getTime(); 74 | cy.clock(now); 75 | cy.seedLocalStorage({ 76 | lastUpdatedTime: new Date('2020-01-01T08:20:00').getTime(), 77 | schemaVersion: '1', 78 | repositories: 'trending-2', 79 | selectedLanguage: 'javascript', 80 | selectedPeriod: 'weekly', 81 | selectedSpokenLanguage: 'en', 82 | }); 83 | cy.fetchReposAndWait(); 84 | cy.fixture('trending').then((json) => { 85 | cy.getLocalStorage('repositories').should('deep.eq', json); 86 | }); 87 | cy.getLocalStorage('selectedLanguage').should('eq', '__ALL__'); 88 | cy.getLocalStorage('selectedPeriod').should('eq', 'daily'); 89 | cy.getLocalStorage('selectedSpokenLanguage').should('eq', '__ALL__'); 90 | cy.getLocalStorage('schemaVersion').should('eq', '2'); 91 | cy.getLocalStorage('lastUpdatedTime').should('eq', now); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /cypress/integration/scroll-icon.js: -------------------------------------------------------------------------------- 1 | describe('Scroll Icon', () => { 2 | it('should scroll to top', () => { 3 | cy.seedLocalStorage(); 4 | cy.visit('/'); 5 | cy.findByLabelText('Scroll to Top Button').should('not.be.visible'); 6 | cy.scrollTo(0, 300); 7 | cy.window().its('pageYOffset').should('eq', 300); 8 | cy.findByLabelText('Scroll to Top Button').should('be.visible'); 9 | cy.findByLabelText('Scroll to Top Button').click(); 10 | cy.window().its('pageYOffset').should('eq', 0); 11 | cy.findByLabelText('Scroll to Top Button').should('not.be.visible'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /cypress/integration/selectors.js: -------------------------------------------------------------------------------- 1 | describe('Selectors', () => { 2 | it('should fetch correct endpoint when change language selector', () => { 3 | cy.fetchReposAndWait(); 4 | cy.findByTestId('top-bar').findByText('All languages').should('be.visible'); 5 | cy.findByTestId('top-bar') 6 | .findByText('Trending today') 7 | .should('be.visible'); 8 | 9 | cy.findByTestId('language-selector').click(); 10 | cy.route({ 11 | method: 'GET', 12 | url: 13 | 'https://ghapi.huchen.dev/repositories?language=javascript&since=daily', 14 | response: 'fixture:javascript', 15 | delay: 100, 16 | }).as('fetchRepos'); 17 | cy.findByTestId('language-selector').findByText('JavaScript').click(); 18 | cy.findAllByTestId('loading-card').should('have.length', 10); 19 | cy.wait('@fetchRepos'); 20 | cy.findByTestId('loading-card').should('not.exist'); 21 | cy.shouldHaveRepoCards(25); 22 | cy.getLocalStorage('selectedLanguage').should('eq', 'javascript'); 23 | cy.fixture('javascript').then((json) => { 24 | cy.getLocalStorage('repositories').should('deep.eq', json); 25 | }); 26 | cy.findByTestId('top-bar').findByText('JavaScript').should('be.visible'); 27 | cy.findByTestId('top-bar') 28 | .findByText('Trending today') 29 | .should('be.visible'); 30 | 31 | cy.findByTestId('period-selector').click(); 32 | cy.route({ 33 | method: 'GET', 34 | url: 35 | 'https://ghapi.huchen.dev/repositories?language=javascript&since=monthly', 36 | response: 'fixture:javascript-monthly', 37 | delay: 100, 38 | }).as('fetchRepos'); 39 | cy.findByTestId('period-selector') 40 | .findByText('Trending this month') 41 | .click(); 42 | cy.findAllByTestId('loading-card').should('have.length', 10); 43 | cy.wait('@fetchRepos'); 44 | cy.findByTestId('loading-card').should('not.exist'); 45 | cy.shouldHaveRepoCards(25); 46 | cy.getLocalStorage('selectedPeriod').should('eq', 'monthly'); 47 | cy.fixture('javascript-monthly').then((json) => { 48 | cy.getLocalStorage('repositories').should('deep.eq', json); 49 | }); 50 | cy.findByTestId('top-bar').findByText('JavaScript').should('be.visible'); 51 | cy.findByTestId('top-bar') 52 | .findByText('Trending this month') 53 | .should('be.visible'); 54 | 55 | // should load from cache 56 | cy.findByTestId('period-selector').click(); 57 | cy.findByTestId('period-selector').findByText('Trending today').click(); 58 | cy.findByTestId('loading-card').should('not.exist'); 59 | cy.shouldHaveRepoCards(25); 60 | cy.fixture('javascript').then((json) => { 61 | cy.getLocalStorage('repositories').should('deep.eq', json); 62 | }); 63 | }); 64 | 65 | it('should fetch correct endpoint when change spoken language selector', () => { 66 | cy.fetchReposAndWait(); 67 | cy.findByTestId('top-bar') 68 | .findByText('All spoken languages') 69 | .should('be.visible'); 70 | cy.findByTestId('top-bar') 71 | .findByText('Trending today') 72 | .should('be.visible'); 73 | 74 | cy.findByTestId('spoken-language-selector').click(); 75 | cy.route({ 76 | method: 'GET', 77 | url: 78 | 'https://ghapi.huchen.dev/repositories?since=daily&spoken_language_code=en', 79 | response: 'fixture:english', 80 | delay: 100, 81 | }).as('fetchRepos'); 82 | cy.findByTestId('spoken-language-selector').findByText('English').click(); 83 | cy.findAllByTestId('loading-card').should('have.length', 10); 84 | cy.wait('@fetchRepos'); 85 | cy.findByTestId('loading-card').should('not.exist'); 86 | cy.shouldHaveRepoCards(25); 87 | cy.getLocalStorage('selectedSpokenLanguage').should('eq', 'en'); 88 | cy.fixture('english').then((json) => { 89 | cy.getLocalStorage('repositories').should('deep.eq', json); 90 | }); 91 | cy.findByTestId('top-bar').findByText('English').should('be.visible'); 92 | cy.findByTestId('top-bar') 93 | .findByText('Trending today') 94 | .should('be.visible'); 95 | 96 | cy.findByTestId('period-selector').click(); 97 | cy.route({ 98 | method: 'GET', 99 | url: 100 | 'https://ghapi.huchen.dev/repositories?since=monthly&spoken_language_code=en', 101 | response: 'fixture:english-monthly', 102 | delay: 100, 103 | }).as('fetchRepos'); 104 | cy.findByTestId('period-selector') 105 | .findByText('Trending this month') 106 | .click(); 107 | cy.findAllByTestId('loading-card').should('have.length', 10); 108 | cy.wait('@fetchRepos'); 109 | cy.findByTestId('loading-card').should('not.exist'); 110 | cy.shouldHaveRepoCards(25); 111 | cy.getLocalStorage('selectedPeriod').should('eq', 'monthly'); 112 | cy.fixture('english-monthly').then((json) => { 113 | cy.getLocalStorage('repositories').should('deep.eq', json); 114 | }); 115 | cy.findByTestId('top-bar').findByText('English').should('be.visible'); 116 | cy.findByTestId('top-bar') 117 | .findByText('Trending this month') 118 | .should('be.visible'); 119 | }); 120 | 121 | it('should have selectors selected correctly when localStorage was set', () => { 122 | cy.seedLocalStorage({ 123 | selectedPeriod: 'monthly', 124 | selectedLanguage: 'javascript', 125 | selectedSpokenLanguage: 'en', 126 | repositories: 'javascript-monthly', 127 | }); 128 | cy.visit('/'); 129 | cy.findByTestId('top-bar').findByText('JavaScript').should('be.visible'); 130 | cy.findByTestId('top-bar').findByText('English').should('be.visible'); 131 | cy.findByTestId('top-bar') 132 | .findByText('Trending this month') 133 | .should('be.visible'); 134 | }); 135 | 136 | it('should have default selectors selected when local storage was not set', () => { 137 | cy.fetchReposAndWait(); 138 | cy.findByTestId('top-bar').findByText('All languages').should('be.visible'); 139 | cy.findByTestId('top-bar') 140 | .findByText('All spoken languages') 141 | .should('be.visible'); 142 | cy.findByTestId('top-bar') 143 | .findByText('Trending today') 144 | .should('be.visible'); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = (on, config) => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | }; 22 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/cypress/add-commands'; 2 | 3 | Cypress.Commands.add( 4 | 'fetchRepos', 5 | ({ response = 'fixture:trending', delay = 0, status = 200 } = {}) => { 6 | cy.server(); 7 | cy.route({ 8 | method: 'GET', 9 | url: 'https://ghapi.huchen.dev/repositories?since=daily', 10 | response, 11 | delay, 12 | status, 13 | }).as('fetchRepos'); 14 | } 15 | ); 16 | 17 | Cypress.Commands.add('waitResponse', () => { 18 | cy.wait('@fetchRepos'); 19 | // eslint-disable-next-line cypress/no-unnecessary-waiting 20 | cy.wait(100); 21 | }); 22 | 23 | Cypress.Commands.add( 24 | 'fetchReposAndWait', 25 | ({ response, delay, status, darkMode = true } = {}) => { 26 | cy.fetchRepos({ response, delay, status }); 27 | cy.visit('/', { 28 | onBeforeLoad(win) { 29 | cy.stub(win, 'matchMedia').returns({ 30 | matches: darkMode, 31 | addListener: () => {}, 32 | }); 33 | }, 34 | }); 35 | cy.waitResponse(); 36 | } 37 | ); 38 | 39 | Cypress.Commands.add('waitAllErrors', () => { 40 | cy.clock(); 41 | cy.wait('@fetchRepos'); 42 | cy.tick(2000); 43 | cy.wait('@fetchRepos'); 44 | cy.tick(4000).invoke('restore'); 45 | cy.wait('@fetchRepos'); 46 | }); 47 | 48 | Cypress.Commands.add('errorFetchReposAndWait', () => { 49 | cy.fetchRepos({ status: 500 }); 50 | cy.visit('/'); 51 | cy.waitAllErrors(); 52 | }); 53 | 54 | Cypress.Commands.add('shouldHaveRepoCards', (num) => { 55 | cy.findByTestId('loaded-repo-list') 56 | .findAllByTestId('repo-card') 57 | .should('have.length', num); 58 | }); 59 | 60 | Cypress.Commands.add('shouldHaveFirstCardContains', (value) => { 61 | cy.findByTestId('loaded-repo-list') 62 | .findAllByTestId('repo-card') 63 | .first() 64 | .contains(value); 65 | }); 66 | 67 | Cypress.Commands.add('getLocalStorage', (key) => { 68 | cy.window().its('localStorage').its(key).then(JSON.parse); 69 | }); 70 | 71 | Cypress.Commands.add('setLocalStorage', (key, value) => 72 | localStorage.setItem(key, JSON.stringify(value)) 73 | ); 74 | 75 | Cypress.Commands.add( 76 | 'seedLocalStorage', 77 | ({ 78 | schemaVersion = '2', 79 | selectedLanguage = '__ALL__', 80 | selectedSpokenLanguage = '__ALL__', 81 | selectedPeriod = 'daily', 82 | repositories = 'trending', 83 | lastUpdatedTime = new Date().getTime() - 10 * 60000, 84 | } = {}) => { 85 | if (typeof schemaVersion !== undefined) { 86 | cy.setLocalStorage('schemaVersion', schemaVersion); 87 | } 88 | if (typeof selectedLanguage !== undefined) { 89 | cy.setLocalStorage('selectedLanguage', selectedLanguage); 90 | } 91 | if (typeof selectedSpokenLanguage !== undefined) { 92 | cy.setLocalStorage('selectedSpokenLanguage', selectedSpokenLanguage); 93 | } 94 | if (typeof selectedPeriod !== undefined) { 95 | cy.setLocalStorage('selectedPeriod', selectedPeriod); 96 | } 97 | if (typeof repositories !== undefined) { 98 | cy.fixture(repositories).then((json) => { 99 | cy.setLocalStorage('repositories', json); 100 | }); 101 | } 102 | if (typeof lastUpdatedTime !== undefined) { 103 | cy.setLocalStorage('lastUpdatedTime', lastUpdatedTime); 104 | } 105 | } 106 | ); 107 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | import { configure } from '@testing-library/cypress'; 17 | import './commands'; 18 | configure({ testIdAttribute: 'data-test-id' }); 19 | -------------------------------------------------------------------------------- /images/1280x640.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenme/hacker-tab-extension/9cfcca84abb3bcc1a15f9403ad147425520df6e6/images/1280x640.jpg -------------------------------------------------------------------------------- /images/1280x640_dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenme/hacker-tab-extension/9cfcca84abb3bcc1a15f9403ad147425520df6e6/images/1280x640_dark.jpg -------------------------------------------------------------------------------- /images/1280x800.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenme/hacker-tab-extension/9cfcca84abb3bcc1a15f9403ad147425520df6e6/images/1280x800.jpg -------------------------------------------------------------------------------- /images/1280x800_dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenme/hacker-tab-extension/9cfcca84abb3bcc1a15f9403ad147425520df6e6/images/1280x800_dark.jpg -------------------------------------------------------------------------------- /images/design.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenme/hacker-tab-extension/9cfcca84abb3bcc1a15f9403ad147425520df6e6/images/design.sketch -------------------------------------------------------------------------------- /images/hero.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenme/hacker-tab-extension/9cfcca84abb3bcc1a15f9403ad147425520df6e6/images/hero.jpeg -------------------------------------------------------------------------------- /images/hero.svg: -------------------------------------------------------------------------------- 1 | code review -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenme/hacker-tab-extension/9cfcca84abb3bcc1a15f9403ad147425520df6e6/images/icon.png -------------------------------------------------------------------------------- /images/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenme/hacker-tab-extension/9cfcca84abb3bcc1a15f9403ad147425520df6e6/images/screenshot.jpg -------------------------------------------------------------------------------- /images/tile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenme/hacker-tab-extension/9cfcca84abb3bcc1a15f9403ad147425520df6e6/images/tile.jpg -------------------------------------------------------------------------------- /images/tile2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenme/hacker-tab-extension/9cfcca84abb3bcc1a15f9403ad147425520df6e6/images/tile2.jpg -------------------------------------------------------------------------------- /images/tile3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenme/hacker-tab-extension/9cfcca84abb3bcc1a15f9403ad147425520df6e6/images/tile3.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hacker-tab-extension", 3 | "version": "0.0.0", 4 | "description": "Browser extension for hackers", 5 | "license": "MIT", 6 | "keywords": [ 7 | "react", 8 | "github", 9 | "browser extension", 10 | "chrome extension" 11 | ], 12 | "main": "src/index.js", 13 | "dependencies": { 14 | "@emotion/core": "^10.0.28", 15 | "@emotion/styled": "^10.0.27", 16 | "@huchenme/github-trending": "^2.4.2", 17 | "append-query": "^2.1.0", 18 | "axios": "^0.19.2", 19 | "date-fns": "^2.14.0", 20 | "emotion-theming": "^10.0.27", 21 | "framer-motion": "^1.11.1", 22 | "husky": "^4.2.5", 23 | "lint-staged": "^10.2.11", 24 | "lodash": "^4.17.15", 25 | "polished": "^3.6.5", 26 | "prop-types": "^15.7.2", 27 | "react": "^16.13.1", 28 | "react-dom": "^16.13.1", 29 | "react-query": "^2.4.13", 30 | "react-scripts": "^3.4.1", 31 | "react-select": "^3.1.0", 32 | "react-spring": "^8.0.27", 33 | "react-use": "^15.3.2" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.10.4", 37 | "@storybook/addon-actions": "^5.3.19", 38 | "@storybook/addon-centered": "^5.3.19", 39 | "@storybook/addon-links": "^5.3.19", 40 | "@storybook/addon-notes": "^5.3.19", 41 | "@storybook/addons": "^5.3.19", 42 | "@storybook/react": "^5.3.19", 43 | "@testing-library/cypress": "^6.0.0", 44 | "@testing-library/jest-dom": "^5.11.0", 45 | "@testing-library/react": "^10.4.3", 46 | "cypress": "^4.9.0", 47 | "eslint-config-cypress": "^0.28.0", 48 | "is-ci-cli": "^2.1.2", 49 | "jest-environment-jsdom-sixteen": "^1.0.3", 50 | "jest-when": "^2.7.2", 51 | "npm-run-all": "^4.1.5", 52 | "prettier": "^2.0.5", 53 | "react-query-devtools": "^2.2.1", 54 | "rimraf": "^3.0.2", 55 | "start-server-and-test": "^1.11.0", 56 | "webpack-cli": "^3.3.12" 57 | }, 58 | "scripts": { 59 | "start": "react-scripts start", 60 | "start:nobrowser": "BROWSER=none react-scripts start", 61 | "storybook": "start-storybook -p 6006", 62 | "test": "is-ci test:coverage test:local", 63 | "test:local": "react-scripts test --env=jest-environment-jsdom-sixteen", 64 | "test:coverage": "react-scripts test --coverage", 65 | "test:e2e": "is-ci test:e2e:run test:e2e:open", 66 | "test:e2e:run": "start-test start:nobrowser 3000 cy:run", 67 | "test:e2e:open": "start-test start:nobrowser 3000 cy:open", 68 | "cy:run": "cypress run", 69 | "cy:open": "cypress open", 70 | "prebuild": "rimraf build", 71 | "build": "npm-run-all build:*", 72 | "build:app": "INLINE_RUNTIME_CHUNK=false react-scripts build", 73 | "build:bg": "webpack --mode production ./src/background/index.js --output ./build/background.js", 74 | "build:bg:dev": "webpack --mode development ./src/background/index.js --output ./build/background.js", 75 | "prezip": "rimraf *.zip", 76 | "zip": "npm-run-all zip:*", 77 | "zip:build": "cd build; zip -r ../build.zip * -x '*.DS_Store'", 78 | "zip:src": "zip -r src.zip src package.json README.md public -x '*.DS_Store'", 79 | "prebuild-storybook": "rimraf storybook-static", 80 | "build-storybook": "build-storybook", 81 | "release": "npm-run-all build zip" 82 | }, 83 | "eslintConfig": { 84 | "extends": "react-app", 85 | "env": { 86 | "browser": true, 87 | "webextensions": true 88 | }, 89 | "rules": { 90 | "no-use-before-define": "off" 91 | } 92 | }, 93 | "browserslist": [ 94 | ">0.2%", 95 | "not dead", 96 | "not ie <= 11", 97 | "not op_mini all" 98 | ], 99 | "prettier": { 100 | "singleQuote": true 101 | }, 102 | "husky": { 103 | "hooks": { 104 | "pre-commit": "lint-staged" 105 | } 106 | }, 107 | "lint-staged": { 108 | "*.{js,jsx,ts,tsx,json,css,scss,md}": [ 109 | "prettier --write" 110 | ] 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /public/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenme/hacker-tab-extension/9cfcca84abb3bcc1a15f9403ad147425520df6e6/public/128.png -------------------------------------------------------------------------------- /public/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenme/hacker-tab-extension/9cfcca84abb3bcc1a15f9403ad147425520df6e6/public/16.png -------------------------------------------------------------------------------- /public/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenme/hacker-tab-extension/9cfcca84abb3bcc1a15f9403ad147425520df6e6/public/48.png -------------------------------------------------------------------------------- /public/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenme/hacker-tab-extension/9cfcca84abb3bcc1a15f9403ad147425520df6e6/public/512.png -------------------------------------------------------------------------------- /public/512_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huchenme/hacker-tab-extension/9cfcca84abb3bcc1a15f9403ad147425520df6e6/public/512_dark.png -------------------------------------------------------------------------------- /public/ga.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // Reference: https://davidsimpson.me/2014/05/27/add-googles-universal-analytics-tracking-chrome-extension/ 3 | (function(i, s, o, g, r, a, m) { 4 | i['GoogleAnalyticsObject'] = r; 5 | (i[r] = 6 | i[r] || 7 | function() { 8 | (i[r].q = i[r].q || []).push(arguments); 9 | }), 10 | (i[r].l = 1 * new Date()); 11 | (a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]); 12 | a.async = 1; 13 | a.src = g; 14 | m.parentNode.insertBefore(a, m); 15 | })( 16 | window, 17 | document, 18 | 'script', 19 | 'https://www.google-analytics.com/analytics.js', 20 | 'ga' 21 | ); // Note: https protocol here 22 | 23 | ga('create', 'UA-134959994-1', 'auto'); 24 | ga('set', 'checkProtocolTask', function() {}); // Removes failing protocol check. @see: http://stackoverflow.com/a/22152353/1958200 25 | ga('require', 'displayfeatures'); 26 | ga('send', 'pageview', '/index.html'); // Specify the virtual path 27 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | New Tab 10 | 44 | 45 | 46 | 47 | 48 | 49 |
50 |
51 | 52 |
53 |
54 | 55 | 56 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Hacker Tab", 4 | "author": "Hu Chen", 5 | "version": "1.10.0", 6 | "description": "Replace browser new tab screen with GitHub trending projects.", 7 | "icons": { 8 | "16": "16.png", 9 | "48": "48.png", 10 | "128": "128.png" 11 | }, 12 | "chrome_url_overrides": { 13 | "newtab": "index.html" 14 | }, 15 | "background": { 16 | "scripts": ["background.js"], 17 | "persistent": false 18 | }, 19 | "permissions": ["storage", "alarms"], 20 | "content_security_policy": "script-src 'self' https://www.google-analytics.com; object-src 'self'" 21 | } 22 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ReactQueryConfigProvider } from 'react-query'; 3 | import { ReactQueryDevtools } from 'react-query-devtools'; 4 | 5 | import Main from './Main'; 6 | 7 | const queryConfig = { 8 | queries: { 9 | retry: 2, 10 | staleTime: 5 * 60 * 1000, 11 | cacheTime: 15 * 60 * 1000, 12 | refetchOnWindowFocus: false, 13 | refetchOnMount: false, 14 | }, 15 | }; 16 | 17 | const App = () => { 18 | return ( 19 | 20 |
21 | 26 | 27 | ); 28 | }; 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /src/Main.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { useState, useEffect } from 'react'; 3 | import styled from '@emotion/styled'; 4 | import { css, jsx } from '@emotion/core'; 5 | import { ThemeProvider } from 'emotion-theming'; 6 | 7 | import { 8 | TopBar, 9 | Footer, 10 | RepositoriesList, 11 | EmptyState, 12 | NetworkError, 13 | BottomIcons, 14 | Fade, 15 | } from './components'; 16 | 17 | import { 18 | useCheckLocalStorageSchema, 19 | useRepositories, 20 | useDarkMode, 21 | } from './hooks'; 22 | import { themeLight, themeDark } from './theme'; 23 | 24 | const Main = () => { 25 | // Clear local storage is schema version not match 26 | useCheckLocalStorageSchema(); 27 | 28 | const [isDark, setIsDark] = useDarkMode(); 29 | 30 | const { 31 | isLoading, 32 | isEmpty, 33 | repositories, 34 | isError, 35 | refetch, 36 | lastUpdatedTime, 37 | selectedLanguage, 38 | selectedPeriod, 39 | selectedSpokenLanguage, 40 | setSelectedLanguage, 41 | setSelectedPeriod, 42 | setSelectedSpokenLanguage, 43 | } = useRepositories(); 44 | 45 | const [showError, setShowError] = useState(false); 46 | 47 | useEffect(() => { 48 | setShowError(isError); 49 | }, [isError]); 50 | 51 | return ( 52 | 53 |
css` 55 | background-color: ${theme.bg}; 56 | transition: background-color ${theme.transition}; 57 | position: relative; 58 | min-height: 100vh; 59 | text-rendering: optimizeLegibility; 60 | -webkit-font-smoothing: antialiased; 61 | `} 62 | > 63 | 64 | 72 | 73 |
80 | 81 |
88 | setShowError(false)} 90 | onReload={() => { 91 | setShowError(false); 92 | refetch(); 93 | }} 94 | /> 95 |
96 |
97 | {!isLoading && isEmpty ? ( 98 |
103 | 107 |
108 | ) : ( 109 | 115 | )} 116 |
117 |
118 | 119 |
120 |
121 | ); 122 | }; 123 | 124 | export default Main; 125 | 126 | const TopBarContainer = styled.div` 127 | position: fixed; 128 | box-sizing: border-box; 129 | top: 0; 130 | width: 100%; 131 | z-index: 20; 132 | `; 133 | -------------------------------------------------------------------------------- /src/background/__tests__/startRequest.js: -------------------------------------------------------------------------------- 1 | import startRequest from '../startRequest'; 2 | import { getObject, setObject } from '../../helpers/localStorage'; 3 | import { fetchRepositories } from '../../helpers/github'; 4 | import { when } from 'jest-when'; 5 | 6 | jest.mock('../../helpers/localStorage'); 7 | jest.mock('../../helpers/github'); 8 | 9 | const RealDate = Date; 10 | 11 | function mockDate(isoDate) { 12 | global.Date = class extends RealDate { 13 | constructor() { 14 | return new RealDate(isoDate); 15 | } 16 | }; 17 | } 18 | 19 | beforeEach(() => { 20 | mockDate('2020-03-11T12:00:00z'); 21 | fetchRepositories.mockClear(); 22 | jest.spyOn(console, 'error'); 23 | jest.spyOn(console, 'log'); 24 | console.error.mockImplementation(() => {}); 25 | console.log.mockImplementation(() => {}); 26 | }); 27 | 28 | afterEach(() => { 29 | global.Date = RealDate; 30 | console.error.mockRestore(); 31 | console.log.mockRestore(); 32 | }); 33 | 34 | test.each` 35 | selectedPeriod | selectedLanguage | selectedSpokenLanguage | expectedPeriod | expectedLanguage | expectedSpokenLanguage 36 | ${'weekly'} | ${'javascript'} | ${'en'} | ${'weekly'} | ${'javascript'} | ${'en'} 37 | ${undefined} | ${undefined} | ${undefined} | ${undefined} | ${undefined} | ${undefined} 38 | ${'weekly'} | ${'__ALL__'} | ${'__ALL__'} | ${'weekly'} | ${undefined} | ${undefined} 39 | `( 40 | 'send correct param to request', 41 | ({ 42 | selectedPeriod, 43 | selectedLanguage, 44 | selectedSpokenLanguage, 45 | expectedPeriod, 46 | expectedLanguage, 47 | expectedSpokenLanguage, 48 | }) => { 49 | when(getObject) 50 | .calledWith('selectedPeriod') 51 | .mockReturnValue(selectedPeriod) 52 | .calledWith('selectedLanguage') 53 | .mockReturnValue(selectedLanguage) 54 | .calledWith('selectedSpokenLanguage') 55 | .mockReturnValue(selectedSpokenLanguage); 56 | startRequest(); 57 | expect(fetchRepositories).toHaveBeenCalledTimes(1); 58 | expect(fetchRepositories).toHaveBeenCalledWith({ 59 | language: expectedLanguage, 60 | since: expectedPeriod, 61 | spoken_language_code: expectedSpokenLanguage, 62 | }); 63 | } 64 | ); 65 | 66 | test('not update localStorage if request fail', async () => { 67 | fetchRepositories.mockRejectedValue(new Error('error')); 68 | await startRequest(); 69 | expect(setObject).toHaveBeenCalledTimes(0); 70 | }); 71 | 72 | test('not update localStorage if response is empty', async () => { 73 | fetchRepositories.mockResolvedValue([]); 74 | await startRequest(); 75 | expect(setObject).toHaveBeenCalledTimes(0); 76 | }); 77 | 78 | test('update localStorage if response is not empty', async () => { 79 | fetchRepositories.mockResolvedValue([ 80 | { id: 1, name: 'a' }, 81 | { id: 2, name: 'b' }, 82 | ]); 83 | await startRequest(); 84 | expect(setObject).toHaveBeenCalledTimes(2); 85 | expect(setObject).toHaveBeenCalledWith('repositories', [ 86 | { id: 1, name: 'a' }, 87 | { id: 2, name: 'b' }, 88 | ]); 89 | expect(setObject).toHaveBeenCalledWith( 90 | 'lastUpdatedTime', 91 | new Date().getTime() 92 | ); 93 | }); 94 | -------------------------------------------------------------------------------- /src/background/index.js: -------------------------------------------------------------------------------- 1 | import { isEmptyList } from '../helpers/github'; 2 | import { KEY_REPOSITORIES, getObject } from '../helpers/localStorage'; 3 | import startRequest from './startRequest'; 4 | 5 | chrome.runtime.onInstalled.addListener(() => { 6 | console.log('onInstalled....'); 7 | scheduleRequest(); 8 | console.log('schedule watchdog alarm to 5 minutes...'); 9 | chrome.alarms.create('watchdog', { periodInMinutes: 5 }); 10 | startRequest(); 11 | }); 12 | 13 | chrome.runtime.onStartup.addListener(() => { 14 | console.log('onStartup....'); 15 | startRequest(); 16 | }); 17 | 18 | chrome.alarms.onAlarm.addListener((alarm) => { 19 | console.log('Alarm triggered', alarm); 20 | if (alarm && alarm.name === 'watchdog') { 21 | chrome.alarms.get('refresh', (alarm) => { 22 | if (alarm) { 23 | console.log('Refresh alarm exists. Yay.'); 24 | const repos = getObject(KEY_REPOSITORIES); 25 | if (isEmptyList(repos)) { 26 | console.log('Refetching because the repo was empty'); 27 | startRequest(); 28 | } 29 | } else { 30 | console.log("Refresh alarm doesn't exist, starting a new one"); 31 | startRequest(); 32 | scheduleRequest(); 33 | } 34 | }); 35 | } else { 36 | startRequest(); 37 | } 38 | }); 39 | 40 | function scheduleRequest() { 41 | console.log('schedule refresh alarm to 30 minutes...'); 42 | chrome.alarms.create('refresh', { periodInMinutes: 30 }); 43 | } 44 | -------------------------------------------------------------------------------- /src/background/startRequest.js: -------------------------------------------------------------------------------- 1 | import { 2 | allLanguagesValue, 3 | allSpokenLanguagesValue, 4 | fetchRepositories, 5 | } from '../helpers/github'; 6 | import { 7 | KEY_REPOSITORIES, 8 | KEY_SELECTED_CODE_LANGUAGE, 9 | KEY_SELECTED_PERIOD, 10 | KEY_SELECTED_SPOKEN_LANGUAGE, 11 | KEY_LAST_UPDATED, 12 | getObject, 13 | setObject, 14 | } from '../helpers/localStorage'; 15 | 16 | export default async function startRequest() { 17 | console.log('start HTTP Request...'); 18 | const period = getObject(KEY_SELECTED_PERIOD); 19 | const lang = getObject(KEY_SELECTED_CODE_LANGUAGE); 20 | const spokenLang = getObject(KEY_SELECTED_SPOKEN_LANGUAGE); 21 | let data = []; 22 | try { 23 | data = await fetchRepositories({ 24 | language: lang === allLanguagesValue ? undefined : lang, 25 | since: period, 26 | spoken_language_code: 27 | spokenLang === allSpokenLanguagesValue ? undefined : spokenLang, 28 | }); 29 | } catch (e) { 30 | console.error(e); 31 | } 32 | if (data && data.length > 0) { 33 | setObject(KEY_REPOSITORIES, data); 34 | setObject(KEY_LAST_UPDATED, new Date().getTime()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/BottomIcons.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import React, { useState, useRef } from 'react'; 3 | import { css, jsx } from '@emotion/core'; 4 | import PropTypes from 'prop-types'; 5 | import styled from '@emotion/styled'; 6 | import { motion, AnimatePresence } from 'framer-motion'; 7 | import { useClickAway } from 'react-use'; 8 | 9 | import { dark } from '../theme'; 10 | import featureToggles from '../feature-toggles'; 11 | import { ReactComponent as MoonIcon } from '../images/moon.svg'; 12 | import { ReactComponent as SunIcon } from '../images/sun.svg'; 13 | import { ReactComponent as SettingIcon } from '../images/setting.svg'; 14 | 15 | import ScrollTop from './ScrollTop'; 16 | 17 | const margin = '20px'; 18 | 19 | export default function BottomIcons({ isDark = false, setIsDark }) { 20 | const [isSettingOpen, setIsSettingOpen] = useState(false); 21 | 22 | const ref = useRef(null); 23 | useClickAway(ref, () => { 24 | setIsSettingOpen(false); 25 | }); 26 | 27 | return ( 28 | 29 |
37 | 43 | { 45 | setIsDark(!isDark); 46 | }} 47 | whileTap={{ scale: 0.8 }} 48 | isRotate={isDark} 49 | aria-label="Toggle Dark Mode Button" 50 | > 51 | 52 | {isDark ? ( 53 | 68 | 76 | 77 | ) : ( 78 | 93 | 101 | 102 | )} 103 | 104 | 105 |
106 | 107 | {Boolean(featureToggles.settings) && ( 108 |
117 | { 122 | setIsSettingOpen(!isSettingOpen); 123 | }} 124 | > 125 | 132 | 133 | 134 | {isSettingOpen && ( 135 | 169 | test 170 | 171 | )} 172 | 173 |
174 | )} 175 |
176 | ); 177 | } 178 | 179 | BottomIcons.propTypes = { 180 | isDark: PropTypes.bool, 181 | setIsDark: PropTypes.func.isRequired, 182 | }; 183 | 184 | const ActionButton = styled(motion.button)` 185 | position: relative; 186 | background-color: transparent; 187 | margin: 0; 188 | padding: 0; 189 | line-height: 1; 190 | transition: color 0.2s cubic-bezier(0.4, 0, 0.2, 1); 191 | cursor: pointer; 192 | border: none; 193 | height: 20px; 194 | width: 20px; 195 | outline: none; 196 | color: ${(props) => props.theme.icon.color}; 197 | 198 | svg { 199 | transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1); 200 | transform: ${(props) => (props.isRotated ? 'rotate(45deg)' : 'none')}; 201 | } 202 | 203 | &:hover { 204 | color: ${(props) => props.theme.icon.hoverColor}; 205 | 206 | svg { 207 | transform: ${(props) => (props.isRotate ? 'rotate(45deg)' : 'none')}; 208 | } 209 | } 210 | `; 211 | -------------------------------------------------------------------------------- /src/components/ContentPlaceholder.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx, keyframes } from '@emotion/core'; 3 | import styled from '@emotion/styled'; 4 | import { linearGradient } from 'polished'; 5 | 6 | const placeHolderShimmer = keyframes` 7 | 0% { 8 | background-position: -468px 0; 9 | } 10 | 100% { 11 | background-position: 468px 0; 12 | } 13 | `; 14 | 15 | const Placeholder = (props) => ( 16 |
css` 18 | width: 100%; 19 | display: inline-block; 20 | display: inline-block; 21 | border-radius: 5px; 22 | animation-duration: 1.5s; 23 | animation-fill-mode: forwards; 24 | animation-iteration-count: infinite; 25 | animation-name: ${placeHolderShimmer}; 26 | animation-timing-function: linear; 27 | background-size: 800px 104px; 28 | height: inherit; 29 | position: relative; 30 | ${linearGradient({ 31 | colorStops: [ 32 | `${theme.loader.stop1} 8%`, 33 | `${theme.loader.stop2} 18%`, 34 | `${theme.loader.stop3} 33%`, 35 | ], 36 | toDirection: 'to right', 37 | fallback: theme.loader.fallback, 38 | })} 39 | `} 40 | {...props} 41 | > 42 |   43 |
44 | ); 45 | 46 | const PlaceholderCard = (props) => ( 47 | 48 | 49 | 58 | 59 | 60 | 61 | <Placeholder 62 | css={css` 63 | width: 150px; 64 | `} 65 | /> 66 | 67 | 68 | 69 | 70 | 71 | 72 | 77 | 78 | 79 | 84 | 85 | 86 | 91 | 92 | 93 | 94 | 95 | 96 | 101 | 102 | 103 | 104 | ); 105 | 106 | const ContentPlaceholder = ({ size = 1 }) => { 107 | return ( 108 |
css` 111 | background: ${theme.card.bg}; 112 | border-radius: 5px; 113 | overflow: hidden; 114 | border: 1px solid ${theme.card.border}; 115 | `} 116 | > 117 | {Array(size) 118 | .fill(null) 119 | .map((_, index) => ( 120 |
css` 124 | border-bottom: 1px solid ${theme.card.divider}; 125 | overflow: hidden; 126 | 127 | :last-of-type { 128 | border-bottom: 0; 129 | } 130 | `} 131 | > 132 | 133 |
134 | ))} 135 |
136 | ); 137 | }; 138 | 139 | export default ContentPlaceholder; 140 | 141 | const Card = styled.div` 142 | width: 720px; 143 | padding: 20px; 144 | box-sizing: border-box; 145 | display: flex; 146 | user-select: none; 147 | `; 148 | 149 | const Left = styled.div` 150 | margin-right: 20px; 151 | `; 152 | 153 | const Middle = styled.div` 154 | flex-grow: 1; 155 | display: flex; 156 | flex-direction: column; 157 | `; 158 | 159 | const Right = styled.div` 160 | margin-left: 30px; 161 | display: flex; 162 | align-items: center; 163 | `; 164 | 165 | const Title = styled.h3` 166 | margin-bottom: 8px; 167 | line-height: 24px; 168 | `; 169 | 170 | const Description = styled.div` 171 | flex-grow: 1; 172 | line-height: 20px; 173 | flex: 1; 174 | `; 175 | 176 | const AdditionalInfo = styled.div` 177 | display: flex; 178 | align-items: center; 179 | font-size: 12px; 180 | `; 181 | 182 | const AdditionalInfoItem = styled.div` 183 | margin-right: 16px; 184 | `; 185 | 186 | const CurrentStar = styled.div` 187 | font-size: 48px; 188 | line-height: 1; 189 | `; 190 | -------------------------------------------------------------------------------- /src/components/EmptyState.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from '@emotion/core'; 3 | import PropTypes from 'prop-types'; 4 | import { useTheme } from 'emotion-theming'; 5 | import LastUpdated from './LastUpdated'; 6 | 7 | import { ReactComponent as EmptyIcon } from '../images/empty.svg'; 8 | 9 | export default function EmptyState({ lastUpdatedTime, onReload }) { 10 | const theme = useTheme(); 11 | 12 | return ( 13 |
14 |
29 | 39 |

45 | Trending repositories results are currently being dissected in{' '} 46 | 52 | GitHub 53 | 54 | . 55 |

56 |

62 | This may be a few minutes. Now would be a great time to write that 63 | novel you have always been talking about. 64 |

65 |
66 | 73 |
74 | ); 75 | } 76 | 77 | EmptyState.propTypes = { 78 | lastUpdatedTime: PropTypes.number, 79 | onReload: PropTypes.func, 80 | }; 81 | -------------------------------------------------------------------------------- /src/components/Fade.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { jsx, css } from '@emotion/core'; 3 | 4 | import { useTransition, animated } from 'react-spring'; 5 | 6 | export default function Fade({ show, children, ...otherProps }) { 7 | const transitions = useTransition(show, null, { 8 | from: { opacity: 0 }, 9 | enter: { opacity: 1 }, 10 | leave: { opacity: 0 }, 11 | }); 12 | 13 | return transitions.map( 14 | ({ item, key, props }) => 15 | item && ( 16 | 24 | {children} 25 | 26 | ) 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Footer.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { useState } from 'react'; 3 | import styled from '@emotion/styled'; 4 | import { css, jsx } from '@emotion/core'; 5 | import { useTheme } from 'emotion-theming'; 6 | import { ReactComponent as HeartIcon } from '../images/heart.svg'; 7 | 8 | export default function Footer() { 9 | const [showEmail, setShowEmail] = useState(false); 10 | const theme = useTheme(); 11 | 12 | return ( 13 |
21 | 22 |
css` 24 | font-size: 16px; 25 | color: ${theme.footer.text}; 26 | display: flex; 27 | align-items: center; 28 | `} 29 | > 30 | Made with 31 | 39 | in Singapore. 40 |
41 |
42 | 43 | setShowEmail(true)}> 44 | {showEmail ? 'chen@huchen.dev' : 'Send Feedback'} 45 | 46 |
css` 48 | ${link}; 49 | color: ${theme.footer.link}; 50 | &:hover { 51 | color: ${theme.footer.linkHover}; 52 | } 53 | `} 54 | onClick={() => { 55 | const confirm = window.confirm( 56 | 'Clear cache will clear your selected languages and settings.' 57 | ); 58 | if (confirm) { 59 | window.localStorage.clear(); 60 | } 61 | }} 62 | > 63 | Clear Cache 64 |
65 | css` 67 | ${link}; 68 | color: ${theme.footer.link}; 69 | &:hover { 70 | color: ${theme.footer.linkHover}; 71 | } 72 | `} 73 | href="https://github.com/huchenme/hacker-tab-extension" 74 | > 75 | GitHub 76 | 77 |
78 |
79 | ); 80 | } 81 | 82 | const Row = styled.div` 83 | display: flex; 84 | justify-content: center; 85 | margin-bottom: 8px; 86 | 87 | &:last-child { 88 | margin-bottom: 0; 89 | } 90 | `; 91 | 92 | const link = css` 93 | margin-right: 24px; 94 | transition: color 0.3s; 95 | cursor: pointer; 96 | font-size: 14px; 97 | text-decoration: underline; 98 | 99 | &:last-child { 100 | margin: 0; 101 | } 102 | `; 103 | 104 | const StyleFeedback = styled.div` 105 | ${link}; 106 | color: ${(props) => 107 | props.showEmail ? props.theme.footer.email : props.theme.footer.link}; 108 | cursor: ${(props) => (props.showEmail ? 'auto' : 'pointer')}; 109 | 110 | &:hover { 111 | color: ${(props) => 112 | props.showEmail 113 | ? props.theme.footer.email 114 | : props.theme.footer.linkHover}; 115 | } 116 | `; 117 | -------------------------------------------------------------------------------- /src/components/Icon.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { css, jsx } from '@emotion/core'; 3 | 4 | const sizes = { 5 | small: '16px', 6 | medium: '24px', 7 | large: '32px', 8 | xlarge: '48px', 9 | }; 10 | 11 | export default function Icon({ 12 | glyph: Glyph, 13 | primaryColor = 'currentColor', 14 | secondaryColor = 'currentColor', 15 | label, 16 | size = 'medium', 17 | onClick, 18 | ...props 19 | }) { 20 | const getSize = size 21 | ? css` 22 | height: ${sizes[size]}; 23 | width: ${sizes[size]}; 24 | ` 25 | : null; 26 | 27 | return ( 28 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/components/InfoItem.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import React from 'react'; 3 | import { css, jsx } from '@emotion/core'; 4 | import { useTheme } from 'emotion-theming'; 5 | 6 | export default function InfoItem({ children, icon }) { 7 | const theme = useTheme(); 8 | return ( 9 |
15 | {icon ? ( 16 |
23 | {React.cloneElement(icon, { 24 | size: 'small', 25 | primaryColor: theme.card.additional, 26 | })} 27 |
28 | ) : null} 29 | {children} 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/LanguageSelect.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { languages, findLanguage, allLanguagesLabel } from '../helpers/github'; 4 | import Select from './Select'; 5 | 6 | const LanguageSelect = ({ onChange, selectedValue }) => ( 7 |
8 | onChange(value)} 13 | options={periodOptions} 14 | placeholder="Select period" 15 | /> 16 |
17 | ); 18 | }; 19 | 20 | PeriodSelect.propTypes = { 21 | selectedValue: PropTypes.string, 22 | onChange: PropTypes.func.isRequired, 23 | }; 24 | 25 | export default React.memo(PeriodSelect); 26 | -------------------------------------------------------------------------------- /src/components/RepositoriesList.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { useState, useEffect } from 'react'; 3 | import styled from '@emotion/styled'; 4 | import { css, jsx } from '@emotion/core'; 5 | import PropTypes from 'prop-types'; 6 | import { useTransition, animated } from 'react-spring'; 7 | 8 | import RepositoryCard from './RepositoryCard'; 9 | import ContentPlaceholder from './ContentPlaceholder'; 10 | import LastUpdated from './LastUpdated'; 11 | import { ReactComponent as RandomIcon } from '../images/random.svg'; 12 | 13 | import { getRandomRepositories } from '../helpers/github'; 14 | 15 | const RepositoriesList = ({ 16 | repositories, 17 | isLoading, 18 | lastUpdatedTime, 19 | onReload, 20 | }) => { 21 | const [random, setRandom] = useState(() => 22 | getRandomRepositories(repositories) 23 | ); 24 | 25 | useEffect(() => { 26 | setRandom(getRandomRepositories(repositories)); 27 | }, [repositories]); 28 | 29 | const transitions = useTransition( 30 | random, 31 | (item) => (item ? item.url : null), 32 | { 33 | from: { opacity: 0 }, 34 | enter: { opacity: 1 }, 35 | leave: { position: 'absolute', opacity: 0 }, 36 | } 37 | ); 38 | 39 | return ( 40 | 41 | {random ? ( 42 |
47 | I’m Feeling Lucky 48 |
54 | {transitions.map(({ item, props, key }) => ( 55 | 56 | 61 | 62 | 63 | 64 | 65 | 66 | ))} 67 |
css` 70 | position: absolute; 71 | right: 0; 72 | top: 50%; 73 | transform: translate(calc(100% + 10px), -50%); 74 | cursor: pointer; 75 | color: ${theme.icon.color}; 76 | transition: color ${theme.transition}; 77 | width: 40px; 78 | height: 40px; 79 | display: flex; 80 | align-items: center; 81 | justify-content: center; 82 | 83 | &:hover { 84 | color: ${theme.icon.hoverColor}; 85 | } 86 | `} 87 | onClick={() => { 88 | setRandom(getRandomRepositories(repositories, random)); 89 | }} 90 | > 91 | 96 |
97 |
98 |
99 | ) : null} 100 |
101 | Trending Repositories 102 | {isLoading ? ( 103 | 104 | ) : ( 105 | 106 | {repositories.map((rep) => ( 107 | 108 | 109 | 110 | ))} 111 | 112 | )} 113 |
114 | {!isLoading ? ( 115 | 122 | ) : null} 123 |
124 | ); 125 | }; 126 | 127 | RepositoriesList.propTypes = { 128 | repositories: PropTypes.array, 129 | isLoading: PropTypes.bool, 130 | lastUpdatedTime: PropTypes.number, 131 | onReload: PropTypes.func, 132 | }; 133 | 134 | RepositoriesList.defaultProps = { 135 | repositories: [], 136 | isLoading: false, 137 | }; 138 | 139 | export default RepositoriesList; 140 | 141 | const Container = styled.div` 142 | margin: 0 auto; 143 | margin-top: 56px; 144 | width: 720px; 145 | `; 146 | 147 | const Title = styled.h1` 148 | text-align: center; 149 | font-family: 'TT Commons', sans-serif; 150 | margin-bottom: 16px; 151 | font-size: 24px; 152 | line-height: 1.1; 153 | transition: color 0.2s ease-in-out; 154 | color: rgba( 155 | ${(props) => (props.theme.isDark ? '255,255,255' : '0,0,0')}, 156 | ${(props) => (props.isLoading ? '0.38' : '0.87')} 157 | ); 158 | `; 159 | 160 | const List = styled.div` 161 | background-color: ${(props) => props.theme.card.bg}; 162 | transition: background-color ${(props) => props.theme.transition}; 163 | border-radius: 5px; 164 | overflow: hidden; 165 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1); 166 | min-height: 120px; 167 | border: 1px solid ${(props) => props.theme.card.border}; 168 | `; 169 | 170 | const Card = styled.div` 171 | border-bottom: 1px solid ${(props) => props.theme.card.divider}; 172 | transition: border-color ${(props) => props.theme.transition}; 173 | overflow: hidden; 174 | :last-of-type { 175 | border-bottom: 0; 176 | } 177 | `; 178 | -------------------------------------------------------------------------------- /src/components/RepositoryCard.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import PropTypes from 'prop-types'; 3 | import styled from '@emotion/styled'; 4 | import { css, jsx } from '@emotion/core'; 5 | import { useTheme } from 'emotion-theming'; 6 | 7 | import { ReactComponent as StarFilledIcon } from '../images/star-filled.svg'; 8 | import { ReactComponent as BitbucketForksIcon } from '../images/forks.svg'; 9 | import { ReactComponent as AuthorIcon } from '../images/author.svg'; 10 | 11 | import Icon from './Icon'; 12 | import InfoItem from './InfoItem'; 13 | 14 | import { getRefUrl, getAvatarString } from '../helpers/github'; 15 | 16 | const RepositoryCard = ({ 17 | stars = 0, 18 | forks = 0, 19 | currentPeriodStars = 0, 20 | url, 21 | avatar, 22 | name, 23 | author, 24 | description, 25 | language, 26 | languageColor, 27 | }) => { 28 | const theme = useTheme(); 29 | 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 |

46 | 54 | 60 | {author} 61 | 62 | 67 | / 68 | 69 | {name} 70 |

71 | {description} 72 | 73 | 74 | {language ? ( 75 | 76 | }> 77 | {language} 78 | 79 | 80 | ) : null} 81 | 82 | }> 83 | {stars.toLocaleString()} 84 | 85 | 86 | 87 | }> 88 | {forks.toLocaleString()} 89 | 90 | 91 | 92 | 93 |
94 | 95 | {currentPeriodStars.toLocaleString()} 96 | 97 |
98 | ); 99 | }; 100 | 101 | RepositoryCard.propTypes = { 102 | author: PropTypes.string, 103 | name: PropTypes.string, 104 | url: PropTypes.string, 105 | avatar: PropTypes.string, 106 | description: PropTypes.string, 107 | language: PropTypes.string, 108 | languageCode: PropTypes.string, 109 | stars: PropTypes.number, 110 | forks: PropTypes.number, 111 | currentPeriodStars: PropTypes.number, 112 | }; 113 | 114 | RepositoryCard.defaultProps = { 115 | languageCode: '#586069', 116 | stars: 0, 117 | forks: 0, 118 | currentPeriodStars: 0, 119 | }; 120 | 121 | export default RepositoryCard; 122 | 123 | const Card = styled.a` 124 | position: relative; 125 | width: 720px; 126 | padding: 20px; 127 | box-sizing: border-box; 128 | display: flex; 129 | transition: background-color ${(props) => props.theme.transition}; 130 | 131 | &, 132 | &:hover, 133 | &:focus, 134 | &:active { 135 | text-decoration: none; 136 | color: initial; 137 | } 138 | 139 | &:hover { 140 | background-color: ${(props) => props.theme.card.bgHover}; 141 | } 142 | `; 143 | 144 | const Left = styled.div` 145 | margin-right: 20px; 146 | `; 147 | 148 | const Middle = styled.div` 149 | flex-grow: 1; 150 | display: flex; 151 | flex-direction: column; 152 | `; 153 | 154 | const Right = styled.div` 155 | margin-left: 30px; 156 | display: flex; 157 | align-items: center; 158 | `; 159 | 160 | const Avatar = styled.img` 161 | height: 40px; 162 | width: 40px; 163 | border-radius: 40px; 164 | overflow: hidden; 165 | border: 0; 166 | vertical-align: bottom; 167 | border: 1px #eef1f3 solid; 168 | `; 169 | 170 | const Description = styled.div` 171 | flex-grow: 1; 172 | font-weight: 400; 173 | color: ${(props) => props.theme.text.helper}; 174 | transition: color ${(props) => props.theme.transition}; 175 | display: -webkit-box; 176 | -webkit-line-clamp: 3; 177 | -webkit-box-orient: vertical; 178 | overflow: hidden; 179 | box-sizing: border-box; 180 | display: inline-block; 181 | max-width: 100%; 182 | overflow: hidden; 183 | text-overflow: ellipsis; 184 | word-wrap: normal; 185 | flex: 1; 186 | `; 187 | 188 | const AdditionalInfo = styled.div` 189 | display: flex; 190 | align-items: center; 191 | color: ${(props) => props.theme.card.additional}; 192 | transition: color ${(props) => props.theme.transition}; 193 | margin-top: 20px; 194 | justify-content: space-between; 195 | `; 196 | 197 | const AdditionalInfoSection = styled.div` 198 | display: flex; 199 | align-items: center; 200 | `; 201 | 202 | const AdditionalInfoItem = styled.div` 203 | margin-right: 24px; 204 | 205 | &:last-child { 206 | margin-right: 0; 207 | } 208 | `; 209 | 210 | const LanguageColor = styled.div` 211 | height: 12px; 212 | width: 12px; 213 | border-radius: 50%; 214 | background-color: ${(props) => props.color || props.theme.card.additional}; 215 | `; 216 | 217 | const CurrentStar = styled.div` 218 | position: relative; 219 | left: -4px; 220 | top: 4px; 221 | font-size: 46px; 222 | line-height: 1; 223 | color: ${(props) => props.theme.card.currentStar}; 224 | transition: color ${(props) => props.theme.transition}; 225 | font-weight: 500; 226 | font-family: 'TT Commons', sans-serif; 227 | `; 228 | -------------------------------------------------------------------------------- /src/components/ScrollTop.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { useState, useEffect } from 'react'; 3 | import { jsx, css } from '@emotion/core'; 4 | import { useSpring } from 'react-spring'; 5 | import useWindowScroll from '../hooks/useWindowScroll'; 6 | import Fade from './Fade'; 7 | 8 | import { ReactComponent as TopIcon } from '../images/top.svg'; 9 | import Icon from './Icon'; 10 | import { useTheme } from 'emotion-theming'; 11 | 12 | export default function ScrollTop(props) { 13 | const [, setY] = useSpring(() => ({ y: 0 })); 14 | const { y } = useWindowScroll(); 15 | const [show, setShow] = useState(false); 16 | const theme = useTheme(); 17 | 18 | useEffect(() => { 19 | if (y > 200) { 20 | setShow(true); 21 | } else { 22 | setShow(false); 23 | } 24 | }, [y]); 25 | 26 | const scrollTop = () => { 27 | setY({ 28 | y: 0, 29 | reset: true, 30 | from: { y: window.scrollY }, 31 | onFrame: (props) => window.scroll(0, props.y), 32 | }); 33 | }; 34 | 35 | return ( 36 | 42 | 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/components/Select.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactSelect from 'react-select'; 3 | import { useTheme } from 'emotion-theming'; 4 | 5 | const Select = (props) => { 6 | const theme = useTheme(); 7 | 8 | return ( 9 | ({ 13 | ...styles, 14 | minHeight: 40, 15 | backgroundColor: theme.select.bg, 16 | borderColor: 'transparent', 17 | boxShadow: 'none', 18 | ':hover': { 19 | ...styles[':hover'], 20 | backgroundColor: theme.select.bgHover, 21 | borderColor: 'transparent', 22 | }, 23 | }), 24 | valueContainer: (styles) => ({ 25 | ...styles, 26 | padding: `2px 6px`, 27 | }), 28 | singleValue: (styles) => ({ 29 | ...styles, 30 | color: theme.select.text, 31 | }), 32 | dropdownIndicator: (styles) => ({ 33 | ...styles, 34 | color: theme.select.indicator, 35 | ':hover': { 36 | ...styles[':hover'], 37 | color: theme.select.indicatorHover, 38 | }, 39 | }), 40 | input: (styles) => ({ 41 | ...styles, 42 | color: theme.select.text, 43 | }), 44 | menu: (styles) => ({ 45 | ...styles, 46 | backgroundColor: theme.select.menu, 47 | }), 48 | option: (styles, { isFocused, isSelected }) => ({ 49 | ...styles, 50 | color: theme.select.text, 51 | cursor: 'pointer', 52 | backgroundColor: isSelected 53 | ? theme.select.menuSelected 54 | : isFocused 55 | ? theme.select.menuFocus 56 | : null, 57 | ':active': { 58 | ...styles[':active'], 59 | backgroundColor: isSelected 60 | ? theme.select.menuSelected 61 | : theme.select.menuFocus, 62 | }, 63 | }), 64 | }} 65 | {...props} 66 | /> 67 | ); 68 | }; 69 | 70 | export default Select; 71 | -------------------------------------------------------------------------------- /src/components/SpokenLanguageSelect.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | findSpokenLanguage, 5 | spokenLanguages, 6 | allSpokenLanguagesLabel, 7 | } from '../helpers/github'; 8 | import Select from './Select'; 9 | 10 | const SpokenLanguageSelect = ({ onChange, selectedValue }) => ( 11 |
12 |