├── .nvmrc
├── .babelrc
├── src
├── fonts
│ ├── index.js
│ └── tt-commons
│ │ ├── TTCommons-Bold.eot
│ │ ├── TTCommons-Bold.ttf
│ │ ├── TTCommons-Thin.eot
│ │ ├── TTCommons-Thin.ttf
│ │ ├── TTCommons-Black.eot
│ │ ├── TTCommons-Black.ttf
│ │ ├── TTCommons-Black.woff
│ │ ├── TTCommons-Bold.woff
│ │ ├── TTCommons-Italic.eot
│ │ ├── TTCommons-Italic.ttf
│ │ ├── TTCommons-Light.eot
│ │ ├── TTCommons-Light.ttf
│ │ ├── TTCommons-Light.woff
│ │ ├── TTCommons-Medium.eot
│ │ ├── TTCommons-Medium.ttf
│ │ ├── TTCommons-Thin.woff
│ │ ├── TTCommons-DemiBold.eot
│ │ ├── TTCommons-DemiBold.ttf
│ │ ├── TTCommons-DemiBold.woff
│ │ ├── TTCommons-ExtraBold.eot
│ │ ├── TTCommons-ExtraBold.ttf
│ │ ├── TTCommons-Italic.woff
│ │ ├── TTCommons-Medium.woff
│ │ ├── TTCommons-Regular.eot
│ │ ├── TTCommons-Regular.ttf
│ │ ├── TTCommons-Regular.woff
│ │ ├── TTCommons-BlackItalic.eot
│ │ ├── TTCommons-BlackItalic.ttf
│ │ ├── TTCommons-BoldItalic.eot
│ │ ├── TTCommons-BoldItalic.ttf
│ │ ├── TTCommons-BoldItalic.woff
│ │ ├── TTCommons-ExtraBold.woff
│ │ ├── TTCommons-ExtraLight.eot
│ │ ├── TTCommons-ExtraLight.ttf
│ │ ├── TTCommons-ExtraLight.woff
│ │ ├── TTCommons-LightItalic.eot
│ │ ├── TTCommons-LightItalic.ttf
│ │ ├── TTCommons-ThinItalic.eot
│ │ ├── TTCommons-ThinItalic.ttf
│ │ ├── TTCommons-ThinItalic.woff
│ │ ├── TTCommons-BlackItalic.woff
│ │ ├── TTCommons-DemiBoldItalic.eot
│ │ ├── TTCommons-DemiBoldItalic.ttf
│ │ ├── TTCommons-LightItalic.woff
│ │ ├── TTCommons-MediumItalic.eot
│ │ ├── TTCommons-MediumItalic.ttf
│ │ ├── TTCommons-MediumItalic.woff
│ │ ├── TTCommons-DemiBoldItalic.woff
│ │ ├── TTCommons-ExtraBoldItalic.eot
│ │ ├── TTCommons-ExtraBoldItalic.ttf
│ │ ├── TTCommons-ExtraBoldItalic.woff
│ │ ├── TTCommons-ExtraLightItalic.eot
│ │ ├── TTCommons-ExtraLightItalic.ttf
│ │ ├── TTCommons-ExtraLightItalic.woff
│ │ └── style.css
├── setupTests.js
├── feature-toggles.js
├── dev-tools
│ ├── load.js
│ └── dev-tools.js
├── images
│ ├── random.svg
│ ├── top.svg
│ ├── cross.svg
│ ├── author.svg
│ ├── heart.svg
│ ├── star-filled.svg
│ ├── warning.svg
│ ├── empty.svg
│ ├── sun.svg
│ ├── forks.svg
│ ├── repos.svg
│ ├── setting.svg
│ ├── moon.svg
│ ├── github.svg
│ ├── logo.svg
│ └── chrome.svg
├── index.js
├── components
│ ├── stories
│ │ ├── NetworkError.stories.js
│ │ ├── Components.stories.js
│ │ └── Icon.stories.js
│ ├── LanguageSelect.js
│ ├── Fade.js
│ ├── PeriodSelect.js
│ ├── SpokenLanguageSelect.js
│ ├── InfoItem.js
│ ├── index.js
│ ├── Icon.js
│ ├── LastUpdated.js
│ ├── ScrollTop.js
│ ├── Select.js
│ ├── EmptyState.js
│ ├── NetworkError.js
│ ├── Footer.js
│ ├── TopBar.js
│ ├── ContentPlaceholder.js
│ ├── RepositoriesList.js
│ ├── RepositoryCard.js
│ └── BottomIcons.js
├── hooks
│ ├── useWindowScroll.js
│ └── useLocalStorage.js
├── helpers
│ ├── localStorage.js
│ └── github.js
├── App.js
├── global.css
├── background
│ ├── startRequest.js
│ ├── index.js
│ └── __tests__
│ │ └── startRequest.js
├── theme.js
├── Main.js
└── hooks.js
├── public
├── 128.png
├── 16.png
├── 48.png
├── 512.png
├── 512_dark.png
├── manifest.json
├── ga.js
└── index.html
├── cypress.json
├── images
├── hero.jpeg
├── icon.png
├── tile.jpg
├── tile2.jpg
├── tile3.jpg
├── 1280x640.jpg
├── 1280x800.jpg
├── design.sketch
├── screenshot.jpg
├── 1280x640_dark.jpg
├── 1280x800_dark.jpg
└── hero.svg
├── .storybook
├── addons.js
└── config.js
├── cypress
├── .eslintrc
├── integration
│ ├── empty-state.js
│ ├── scroll-icon.js
│ ├── dark-mode.js
│ ├── error-state.js
│ ├── feeling-lucky.js
│ ├── load-repositories.js
│ └── selectors.js
├── support
│ ├── index.js
│ └── commands.js
├── plugins
│ └── index.js
└── fixtures
│ ├── trending.json
│ └── trending-2.json
├── .github
├── workflows
│ ├── unit.yml
│ └── cypress.yml
└── FUNDING.yml
├── .gitignore
├── LICENSE
├── README.md
└── package.json
/.nvmrc:
--------------------------------------------------------------------------------
1 | v12.10.0
2 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react-app"]
3 | }
4 |
--------------------------------------------------------------------------------
/src/fonts/index.js:
--------------------------------------------------------------------------------
1 | import './tt-commons/style.css';
2 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 |
--------------------------------------------------------------------------------
/public/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/public/128.png
--------------------------------------------------------------------------------
/public/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/public/16.png
--------------------------------------------------------------------------------
/public/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/public/48.png
--------------------------------------------------------------------------------
/public/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/public/512.png
--------------------------------------------------------------------------------
/src/feature-toggles.js:
--------------------------------------------------------------------------------
1 | const featureToggles = {};
2 |
3 | export default featureToggles;
4 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:3000",
3 | "projectId": "wrmpdh"
4 | }
5 |
--------------------------------------------------------------------------------
/images/hero.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/images/hero.jpeg
--------------------------------------------------------------------------------
/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/images/icon.png
--------------------------------------------------------------------------------
/images/tile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/images/tile.jpg
--------------------------------------------------------------------------------
/images/tile2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/images/tile2.jpg
--------------------------------------------------------------------------------
/images/tile3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/images/tile3.jpg
--------------------------------------------------------------------------------
/images/1280x640.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/images/1280x640.jpg
--------------------------------------------------------------------------------
/images/1280x800.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/images/1280x800.jpg
--------------------------------------------------------------------------------
/public/512_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/public/512_dark.png
--------------------------------------------------------------------------------
/images/design.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/images/design.sketch
--------------------------------------------------------------------------------
/images/screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/images/screenshot.jpg
--------------------------------------------------------------------------------
/images/1280x640_dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/images/1280x640_dark.jpg
--------------------------------------------------------------------------------
/images/1280x800_dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/images/1280x800_dark.jpg
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Bold.eot
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Bold.ttf
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Thin.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Thin.eot
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Thin.ttf
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Black.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Black.eot
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Black.ttf
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Black.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Black.woff
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Bold.woff
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Italic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Italic.eot
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Italic.ttf
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Light.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Light.eot
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Light.ttf
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Light.woff
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Medium.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Medium.eot
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Medium.ttf
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Thin.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Thin.woff
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-DemiBold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-DemiBold.eot
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-DemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-DemiBold.ttf
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-DemiBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-DemiBold.woff
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-ExtraBold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-ExtraBold.eot
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-ExtraBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-ExtraBold.ttf
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Italic.woff
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Medium.woff
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Regular.eot
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Regular.ttf
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-Regular.woff
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-BlackItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-BlackItalic.eot
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-BlackItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-BlackItalic.ttf
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-BoldItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-BoldItalic.eot
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-BoldItalic.ttf
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-BoldItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-BoldItalic.woff
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-ExtraBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-ExtraBold.woff
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-ExtraLight.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-ExtraLight.eot
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-ExtraLight.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-ExtraLight.ttf
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-ExtraLight.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-ExtraLight.woff
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-LightItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-LightItalic.eot
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-LightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-LightItalic.ttf
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-ThinItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-ThinItalic.eot
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-ThinItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-ThinItalic.ttf
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-ThinItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-ThinItalic.woff
--------------------------------------------------------------------------------
/.storybook/addons.js:
--------------------------------------------------------------------------------
1 | import '@storybook/addon-actions/register';
2 | import '@storybook/addon-links/register';
3 | import '@storybook/addon-notes/register';
4 |
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-BlackItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-BlackItalic.woff
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-DemiBoldItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-DemiBoldItalic.eot
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-DemiBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-DemiBoldItalic.ttf
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-LightItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-LightItalic.woff
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-MediumItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-MediumItalic.eot
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-MediumItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-MediumItalic.ttf
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-MediumItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-MediumItalic.woff
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-DemiBoldItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-DemiBoldItalic.woff
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-ExtraBoldItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-ExtraBoldItalic.eot
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-ExtraBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-ExtraBoldItalic.ttf
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-ExtraBoldItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-ExtraBoldItalic.woff
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-ExtraLightItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-ExtraLightItalic.eot
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-ExtraLightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-ExtraLightItalic.ttf
--------------------------------------------------------------------------------
/src/fonts/tt-commons/TTCommons-ExtraLightItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huchenme/hacker-tab-extension/HEAD/src/fonts/tt-commons/TTCommons-ExtraLightItalic.woff
--------------------------------------------------------------------------------
/cypress/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "env": { "cypress/globals": true },
4 | "extends": ["react-app", "plugin:cypress/recommended"],
5 | "plugins": ["cypress"]
6 | }
7 |
--------------------------------------------------------------------------------
/src/dev-tools/load.js:
--------------------------------------------------------------------------------
1 | function load(callback) {
2 | if (process.env.NODE_ENV === 'development') {
3 | import('./dev-tools').finally(callback);
4 | } else {
5 | callback();
6 | }
7 | }
8 |
9 | export default load;
10 |
--------------------------------------------------------------------------------
/src/images/random.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/dev-tools/dev-tools.js:
--------------------------------------------------------------------------------
1 | const requireDevToolsLocal = require.context(
2 | './',
3 | false,
4 | /dev-tools\.local\.js/
5 | );
6 | const local = requireDevToolsLocal.keys()[0];
7 | if (local) {
8 | requireDevToolsLocal(local);
9 | }
10 |
--------------------------------------------------------------------------------
/src/images/top.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/images/cross.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/images/author.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/images/heart.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/images/star-filled.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import loadDevTools from './dev-tools/load';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 |
5 | import App from './App';
6 |
7 | import './fonts';
8 | import './global.css';
9 |
10 | const rootElement = document.getElementById('root');
11 | loadDevTools(() => {
12 | ReactDOM.render(, rootElement);
13 | });
14 |
--------------------------------------------------------------------------------
/src/images/warning.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/images/empty.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/images/sun.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/components/stories/NetworkError.stories.js:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import { jsx, css } from '@emotion/core';
3 | import { storiesOf } from '@storybook/react';
4 | import { action } from '@storybook/addon-actions';
5 |
6 | import NetworkError from '../NetworkError';
7 |
8 | storiesOf('NetworkError', module).add('basic usage', () => (
9 |
15 |
16 |
17 | ));
18 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/images/forks.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/images/repos.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/images/setting.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/hooks/useWindowScroll.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | const useWindowScroll = () => {
4 | const [state, setState] = useState({
5 | x: window.scrollX,
6 | y: window.scrollY,
7 | });
8 |
9 | useEffect(() => {
10 | const handler = () => {
11 | setState({
12 | x: window.scrollX,
13 | y: window.scrollY,
14 | });
15 | };
16 |
17 | window.addEventListener('scroll', handler);
18 |
19 | return () => {
20 | window.removeEventListener('scroll', handler);
21 | };
22 | }, []);
23 |
24 | return state;
25 | };
26 |
27 | export default useWindowScroll;
28 |
--------------------------------------------------------------------------------
/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/helpers/localStorage.js:
--------------------------------------------------------------------------------
1 | export const CURRENT_SCHEMA_VERSION = '2';
2 |
3 | export const KEY_REPOSITORIES = 'repositories';
4 | export const KEY_LAST_UPDATED = 'lastUpdatedTime';
5 | export const KEY_SELECTED_CODE_LANGUAGE = 'selectedLanguage';
6 | export const KEY_SELECTED_SPOKEN_LANGUAGE = 'selectedSpokenLanguage';
7 | export const KEY_SELECTED_PERIOD = 'selectedPeriod';
8 | export const KEY_SCHEMA_VERSION = 'schemaVersion';
9 | export const KEY_DARK_MODE = 'preferDarkMode';
10 |
11 | export const getObject = (key) => JSON.parse(localStorage.getItem(key));
12 | export const setObject = (key, value) =>
13 | localStorage.setItem(key, JSON.stringify(value));
14 |
--------------------------------------------------------------------------------
/src/images/moon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
15 | );
16 |
17 | LanguageSelect.propTypes = {
18 | selectedValue: PropTypes.string,
19 | onChange: PropTypes.func.isRequired,
20 | };
21 |
22 | export default React.memo(LanguageSelect);
23 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/PeriodSelect.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Select from './Select';
4 | import { periodOptions, findPeriod } from '../helpers/github';
5 |
6 | const PeriodSelect = ({ onChange, selectedValue }) => {
7 | return (
8 |
9 |
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/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/components/stories/Components.stories.js:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import { useState } from 'react';
3 | import { css, jsx } from '@emotion/core';
4 | import { storiesOf } from '@storybook/react';
5 | import { action } from '@storybook/addon-actions';
6 |
7 | import { EmptyState, Footer, ContentPlaceholder } from '..';
8 |
9 | storiesOf('EmptyState', module).add('default', () => );
10 |
11 | storiesOf('Footer', module).add('default', () => );
12 |
13 | storiesOf('ContentPlaceholder', module)
14 | .add('size 1', () => )
15 | .add('size 10', () => );
16 |
17 | export const redBox = css`
18 | padding: 10px;
19 | background: white;
20 | border: 2px solid red;
21 | display: inline-block;
22 | `;
23 |
--------------------------------------------------------------------------------
/src/components/stories/Icon.stories.js:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import { jsx } from '@emotion/core';
3 | import { storiesOf } from '@storybook/react';
4 |
5 | import Icon from '../Icon';
6 |
7 | import { ReactComponent as StarFilledIcon } from '../../images/star-filled.svg';
8 |
9 | storiesOf('Icon', module)
10 | .add('basic usage', () => )
11 | .add('change color', () => (
12 |
13 |
14 |
15 | ))
16 | .add('size small', () => (
17 |
18 |
19 |
20 |
21 |
22 |
23 | ));
24 |
--------------------------------------------------------------------------------
/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 |
19 | );
20 |
21 | SpokenLanguageSelect.propTypes = {
22 | selectedValue: PropTypes.string,
23 | onChange: PropTypes.func.isRequired,
24 | };
25 |
26 | export default React.memo(SpokenLanguageSelect);
27 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/global.css:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | p,
4 | div,
5 | h1,
6 | h2,
7 | h3,
8 | h4,
9 | h5,
10 | h6,
11 | ul,
12 | ol,
13 | dl,
14 | img,
15 | pre,
16 | form,
17 | fieldset {
18 | margin: 0;
19 | padding: 0;
20 | }
21 | img,
22 | fieldset {
23 | border: 0;
24 | }
25 |
26 | body,
27 | html {
28 | height: 100%;
29 | width: 100%;
30 | }
31 |
32 | body {
33 | color: #172b4d;
34 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
35 | sans-serif;
36 | font-size: 14px;
37 | font-style: normal;
38 | line-height: 1.5;
39 | -ms-overflow-style: -ms-autohiding-scrollbar;
40 | text-decoration-skip-ink: auto;
41 | }
42 |
43 | ::-webkit-scrollbar {
44 | width: 0;
45 | height: 0;
46 | background: 0 0;
47 | }
48 |
49 | h1,
50 | h2,
51 | h3,
52 | h4,
53 | h5,
54 | h6 {
55 | font-weight: 500;
56 | }
57 |
--------------------------------------------------------------------------------
/src/images/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as EmptyState } from './EmptyState';
2 | export { default as Footer } from './Footer';
3 | export { default as InfoItem } from './InfoItem';
4 | export { default as LanguageSelect } from './LanguageSelect';
5 | export { default as PeriodSelect } from './PeriodSelect';
6 | export { default as SpokenLanguageSelect } from './SpokenLanguageSelect';
7 | export { default as RepositoriesList } from './RepositoriesList';
8 | export { default as RepositoryCard } from './RepositoryCard';
9 | export { default as TopBar } from './TopBar';
10 | export { default as ContentPlaceholder } from './ContentPlaceholder';
11 | export { default as Icon } from './Icon';
12 | export { default as NetworkError } from './NetworkError';
13 | export { default as ScrollTop } from './ScrollTop';
14 | export { default as Fade } from './Fade';
15 | export { default as BottomIcons } from './BottomIcons';
16 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/images/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/hooks/useLocalStorage.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | const useLocalStorage = (key, initialValue) => {
4 | const [state, setState] = useState(() => {
5 | try {
6 | const localStorageValue = localStorage.getItem(key);
7 | if (typeof localStorageValue !== 'string') {
8 | if (initialValue !== undefined) {
9 | localStorage.setItem(key, JSON.stringify(initialValue));
10 | }
11 | return initialValue;
12 | } else {
13 | return JSON.parse(localStorageValue ?? 'null');
14 | }
15 | } catch {
16 | // If user is in private mode or has storage restriction
17 | // localStorage can throw. JSON.parse and JSON.stringify
18 | // can throw, too.
19 | return initialValue;
20 | }
21 | });
22 |
23 | const serializedState = JSON.stringify(state);
24 | useEffect(() => {
25 | try {
26 | localStorage.setItem(key, serializedState);
27 | } catch {
28 | // If user is in private mode or has storage restriction
29 | // localStorage can throw. Also JSON.stringify can throw.
30 | }
31 | // eslint-disable-next-line react-hooks/exhaustive-deps
32 | }, [serializedState]);
33 |
34 | return [state, setState];
35 | };
36 |
37 | export default useLocalStorage;
38 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/src/images/chrome.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 | New Tab
10 |
44 |
45 |
46 |
47 |
48 |
49 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/components/LastUpdated.js:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import { useState, useEffect } from 'react';
3 | import { css, jsx } from '@emotion/core';
4 | import PropTypes from 'prop-types';
5 | import { useTheme } from 'emotion-theming';
6 | import formatDistanceToNow from 'date-fns/formatDistanceToNow';
7 |
8 | function formatTime(time) {
9 | return time
10 | ? formatDistanceToNow(new Date(time), {
11 | addSuffix: true,
12 | })
13 | : undefined;
14 | }
15 |
16 | const LastUpdated = ({ lastUpdatedTime, onReload, ...otherProps }) => {
17 | const theme = useTheme();
18 |
19 | const [lastUpdatedString, setLastUpdatedString] = useState(
20 | formatTime(lastUpdatedTime)
21 | );
22 |
23 | useEffect(() => {
24 | setLastUpdatedString(formatTime(lastUpdatedTime));
25 | const intervalId = setInterval(() => {
26 | setLastUpdatedString(formatTime(lastUpdatedTime));
27 | }, 1000 * 10);
28 |
29 | return () => {
30 | clearInterval(intervalId);
31 | };
32 | }, [lastUpdatedTime]);
33 |
34 | if (!lastUpdatedString) {
35 | return null;
36 | }
37 |
38 | return (
39 |
47 | Last updated
48 |
62 | {lastUpdatedString}
63 |
64 |
65 | );
66 | };
67 |
68 | LastUpdated.propTypes = {
69 | lastUpdatedTime: PropTypes.number,
70 | onReload: PropTypes.func,
71 | };
72 |
73 | export default LastUpdated;
74 |
--------------------------------------------------------------------------------
/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/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/NetworkError.js:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import { css, jsx } from '@emotion/core';
3 | import { useTheme } from 'emotion-theming';
4 | import { ReactComponent as Warning } from '../images/warning.svg';
5 | import { ReactComponent as Close } from '../images/cross.svg';
6 | import Icon from './Icon';
7 |
8 | export default function NetworkError({ onReload, onClose }) {
9 | const theme = useTheme();
10 | return (
11 |
25 |
30 |
31 |
32 |
37 | It seems there are some issues while loading data
38 |
39 |
44 |
65 |
66 |
73 |
74 |
75 |
76 | );
77 | }
78 |
79 | const buttonReset = css`
80 | margin: 0;
81 | cursor: pointer;
82 | display: inline-flex;
83 | outline: none;
84 | padding: 0;
85 | position: relative;
86 | align-items: center;
87 | border-radius: 0;
88 | vertical-align: middle;
89 | appearance: none;
90 | justify-content: center;
91 | text-decoration: none;
92 | user-select: none;
93 | background-color: transparent;
94 | box-sizing: border-box;
95 | border: 0;
96 | transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
97 | `;
98 |
--------------------------------------------------------------------------------
/src/fonts/tt-commons/style.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'TT Commons';
3 | src: url('TTCommons-Regular.eot');
4 | src: local('TT Commons Regular'), local('TTCommons-Regular'),
5 | url('TTCommons-Regular.eot?#iefix') format('embedded-opentype'),
6 | url('TTCommons-Regular.woff') format('woff'),
7 | url('TTCommons-Regular.ttf') format('truetype');
8 | font-weight: normal;
9 | font-style: normal;
10 | }
11 |
12 | @font-face {
13 | font-family: 'TT Commons';
14 | src: url('TTCommons-Italic.eot');
15 | src: local('TT Commons Italic'), local('TTCommons-Italic'),
16 | url('TTCommons-Italic.eot?#iefix') format('embedded-opentype'),
17 | url('TTCommons-Italic.woff') format('woff'),
18 | url('TTCommons-Italic.ttf') format('truetype');
19 | font-weight: normal;
20 | font-style: italic;
21 | }
22 |
23 | @font-face {
24 | font-family: 'TT Commons';
25 | src: url('TTCommons-Medium.eot');
26 | src: local('TT Commons Medium'), local('TTCommons-Medium'),
27 | url('TTCommons-Medium.eot?#iefix') format('embedded-opentype'),
28 | url('TTCommons-Medium.woff') format('woff'),
29 | url('TTCommons-Medium.ttf') format('truetype');
30 | font-weight: 500;
31 | font-style: normal;
32 | }
33 |
34 | @font-face {
35 | font-family: 'TT Commons';
36 | src: url('TTCommons-DemiBoldItalic.eot');
37 | src: local('TT Commons DemiBold Italic'), local('TTCommons-DemiBoldItalic'),
38 | url('TTCommons-DemiBoldItalic.eot?#iefix') format('embedded-opentype'),
39 | url('TTCommons-DemiBoldItalic.woff') format('woff'),
40 | url('TTCommons-DemiBoldItalic.ttf') format('truetype');
41 | font-weight: bold;
42 | font-style: italic;
43 | }
44 |
45 | @font-face {
46 | font-family: 'TT Commons';
47 | src: url('TTCommons-DemiBold.eot');
48 | src: local('TT Commons DemiBold'), local('TTCommons-DemiBold'),
49 | url('TTCommons-DemiBold.eot?#iefix') format('embedded-opentype'),
50 | url('TTCommons-DemiBold.woff') format('woff'),
51 | url('TTCommons-DemiBold.ttf') format('truetype');
52 | font-weight: bold;
53 | font-style: normal;
54 | }
55 |
56 | @font-face {
57 | font-family: 'TT Commons';
58 | src: url('TTCommons-ExtraBold.eot');
59 | src: local('TT Commons ExtraBold'), local('TTCommons-ExtraBold'),
60 | url('TTCommons-ExtraBold.eot?#iefix') format('embedded-opentype'),
61 | url('TTCommons-ExtraBold.woff') format('woff'),
62 | url('TTCommons-ExtraBold.ttf') format('truetype');
63 | font-weight: 800;
64 | font-style: normal;
65 | }
66 |
67 | @font-face {
68 | font-family: 'TT Commons';
69 | src: url('TTCommons-Black.eot');
70 | src: local('TT Commons Black'), local('TTCommons-Black'),
71 | url('TTCommons-Black.eot?#iefix') format('embedded-opentype'),
72 | url('TTCommons-Black.woff') format('woff'),
73 | url('TTCommons-Black.ttf') format('truetype');
74 | font-weight: 900;
75 | font-style: normal;
76 | }
77 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://chrome.google.com/webstore/detail/hacker-tab/ibomigipadcieapbemkegkmadbbanbgm)
2 | [](https://chrome.google.com/webstore/detail/hacker-tab/ibomigipadcieapbemkegkmadbbanbgm/reviews)
3 | [](https://travis-ci.org/huchenme/hacker-tab-extension)
4 | [](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 | 
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 |
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 |
--------------------------------------------------------------------------------
/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/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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
1 | import { mix } from 'polished';
2 |
3 | const darkBaseColor = '#121212';
4 |
5 | export const dark = (level = 0, color = '#fff') =>
6 | mix(level / 100, color, darkBaseColor);
7 |
8 | const baseTheme = {
9 | transition: '.2s cubic-bezier(0.4, 0, 0.2, 1)',
10 | };
11 |
12 | export const themeLight = {
13 | ...baseTheme,
14 | isDark: false,
15 | bg: '#eee',
16 | text: {
17 | active: 'rgba(0, 0, 0, 0.87)',
18 | helper: 'rgba(0, 0, 0, 0.60)',
19 | disabled: 'rgba(0, 0, 0, 0.38)',
20 | },
21 | rgba: (opacity) => `rgba(0, 0, 0, ${opacity})`,
22 | topBar: {
23 | bg: '#fff',
24 | },
25 | card: {
26 | bg: '#fff',
27 | bgHover: 'rgba(0, 0, 0, 0.04)',
28 | title: 'rgba(0, 0, 0, 0.87)',
29 | currentStar: 'rgba(0, 0, 0, 0.20)',
30 | additional: '#586069',
31 | divider: '#e8e8e8',
32 | border: 'transparent',
33 | },
34 | icon: {
35 | color: '#aaa',
36 | hoverColor: '#777',
37 | },
38 | error: {
39 | bg: '#ffa000',
40 | },
41 | footer: {
42 | text: '#aaa',
43 | link: '#aaa',
44 | linkHover: '#777',
45 | email: '#1976D2',
46 | heart: '#F44336',
47 | },
48 | select: {
49 | bg: '#EBECF0',
50 | bgHover: '#EBECF0',
51 | text: 'rgba(0, 0, 0, 0.87)',
52 | label: 'rgba(0, 0, 0, 0.60)',
53 | indicator: 'rgba(0, 0, 0, 0.60)',
54 | indicatorHover: 'rgba(0, 0, 0, 0.87)',
55 | menu: '#fff',
56 | menuFocus: '#EEEEEE',
57 | menuSelected: '#DFE0DF',
58 | },
59 | loader: {
60 | stop1: 'rgba(0,0,0,0.03)',
61 | stop2: 'rgba(0,0,0,0.07)',
62 | stop3: 'rgba(0,0,0,0.03)',
63 | fallback: '#f6f7f8',
64 | },
65 | emptyState: {
66 | bg: '#fafbfc',
67 | border: '#e1e4e8',
68 | icon: '#a3aab1',
69 | title: 'rgba(0, 0, 0, 0.87)',
70 | text: 'rgba(0, 0, 0, 0.60)',
71 | },
72 | };
73 |
74 | export const themeDark = {
75 | ...baseTheme,
76 | isDark: true,
77 | bg: dark(0),
78 | text: {
79 | active: 'rgba(255, 255, 255, 0.87)',
80 | helper: 'rgba(255, 255, 255, 0.60)',
81 | disabled: 'rgba(255, 255, 255, 0.38)',
82 | },
83 | rgba: (opacity) => `rgba(255, 255, 255, ${opacity})`,
84 | topBar: {
85 | bg: dark(9),
86 | },
87 | card: {
88 | bg: dark(5),
89 | bgHover: dark(9),
90 | title: '#fff',
91 | currentStar: 'rgba(255, 255, 255, 0.38)',
92 | additional: 'rgba(255, 255, 255, 0.87)',
93 | divider: dark(12),
94 | border: dark(12),
95 | },
96 | icon: {
97 | color: 'rgba(255, 255, 255, 0.38)',
98 | hoverColor: 'rgba(255, 255, 255, 0.87)',
99 | },
100 | error: {
101 | bg: dark(24, '#ffa000'),
102 | },
103 | footer: {
104 | text: 'rgba(255, 255, 255, 0.87)',
105 | link: 'rgba(255, 255, 255, 0.60)',
106 | linkHover: '#fff',
107 | email: '#90CAF9',
108 | heart: '#EF9A9A',
109 | },
110 | select: {
111 | bg: 'rgba(255, 255, 255, 0.05)',
112 | bgHover: 'rgba(255, 255, 255, 0.12)',
113 | text: 'rgba(255, 255, 255, 0.87)',
114 | label: 'rgba(255, 255, 255, 0.38)',
115 | indicator: 'rgba(255, 255, 255, 0.60)',
116 | indicatorHover: 'rgba(255, 255, 255, 0.87)',
117 | menu: '#424242',
118 | menuFocus: '#585858',
119 | menuSelected: '#6C6C6C',
120 | },
121 | loader: {
122 | stop1: 'rgba(255,255,255,0.03)',
123 | stop2: 'rgba(255,255,255,0.07)',
124 | stop3: 'rgba(255,255,255,0.03)',
125 | fallback: dark(14),
126 | },
127 | emptyState: {
128 | bg: dark(5),
129 | border: 'transparent',
130 | icon: 'rgba(255, 255, 255, 0.60)',
131 | title: 'rgba(255, 255, 255, 0.87)',
132 | text: 'rgba(255, 255, 255, 0.60)',
133 | },
134 | };
135 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/helpers/github.js:
--------------------------------------------------------------------------------
1 | import { find, sample, uniqBy, compact, snakeCase } from 'lodash';
2 | import axios from 'axios';
3 | import appendQuery from 'append-query';
4 | import {
5 | languages as apiLanguages,
6 | spokenLanguages as apiSpokenLanguages,
7 | } from '@huchenme/github-trending';
8 |
9 | export const periodOptions = [
10 | { value: 'daily', label: 'Trending today' },
11 | { value: 'weekly', label: 'Trending this week' },
12 | { value: 'monthly', label: 'Trending this month' },
13 | ];
14 |
15 | export const findPeriod = (value) => find(periodOptions, { value });
16 |
17 | export const getRandomRepositories = (repositories = [], current) => {
18 | if (repositories.length < 2 || !current) {
19 | return sample(repositories);
20 | }
21 | const otherRepos = repositories.filter(
22 | (repo) => repo.author !== current.author && repo.name !== current.name
23 | );
24 | return sample(otherRepos);
25 | };
26 |
27 | export const getRefUrl = (url = '/') =>
28 | appendQuery(url, 'ref=HackerTabExtension');
29 |
30 | export const getAvatarString = (src, size = 160) =>
31 | src ? `${src}?s=${size}` : undefined;
32 |
33 | export const allLanguagesValue = '__ALL__';
34 |
35 | export const allLanguagesLabel = 'All languages';
36 |
37 | export const allLanguagesOption = {
38 | label: allLanguagesLabel,
39 | value: allLanguagesValue,
40 | };
41 |
42 | const popularLanguages = [
43 | 'C++',
44 | 'HTML',
45 | 'Java',
46 | 'JavaScript',
47 | 'PHP',
48 | 'Python',
49 | 'Ruby',
50 | 'CSS',
51 | 'Objective-C',
52 | 'Swift',
53 | 'TypeScript',
54 | ];
55 |
56 | export const languages = [
57 | allLanguagesOption,
58 | ...uniqBy(
59 | compact([
60 | ...popularLanguages.map((lang) => find(apiLanguages, { name: lang })),
61 | ...apiLanguages,
62 | ]),
63 | 'name'
64 | ).map(({ urlParam, name }) => ({
65 | label: name,
66 | value: urlParam,
67 | })),
68 | ];
69 |
70 | export const findLanguage = (value) =>
71 | find(languages, { value }) || allLanguagesOption;
72 |
73 | export const isEmptyList = (list) => !list || list.length === 0;
74 |
75 | export const allSpokenLanguagesValue = '__ALL__';
76 |
77 | export const allSpokenLanguagesLabel = 'All spoken languages';
78 |
79 | export const allSpokenLanguagesOption = {
80 | label: allSpokenLanguagesLabel,
81 | value: allSpokenLanguagesValue,
82 | };
83 |
84 | // taken from https://octoverse.github.com/
85 | const popularSpokenLanguages = [
86 | 'English',
87 | 'Chinese',
88 | 'Hindi',
89 | 'German',
90 | 'Japanese',
91 | 'French',
92 | 'Russian',
93 | 'Portuguese',
94 | 'Dutch, Flemish',
95 | 'Korean',
96 | 'Spanish, Castilian',
97 | 'Turkish',
98 | ];
99 |
100 | export const spokenLanguages = [
101 | allSpokenLanguagesOption,
102 | ...uniqBy(
103 | compact([
104 | ...popularSpokenLanguages.map((lang) =>
105 | find(apiSpokenLanguages, { name: lang })
106 | ),
107 | ...apiSpokenLanguages,
108 | ]),
109 | 'name'
110 | ).map(({ urlParam, name }) => ({
111 | label: name,
112 | value: urlParam,
113 | })),
114 | ];
115 |
116 | export const findSpokenLanguage = (value) =>
117 | find(spokenLanguages, { value }) || allSpokenLanguagesOption;
118 |
119 | export function buildUrl(baseUrl, params = {}) {
120 | const queryString = Object.keys(params)
121 | .filter((key) => params[key])
122 | .map((key) => `${snakeCase(key)}=${params[key]}`)
123 | .join('&');
124 |
125 | return queryString === '' ? baseUrl : `${baseUrl}?${queryString}`;
126 | }
127 |
128 | export async function fetchRepositories(params = {}) {
129 | const { data, status } = await axios(
130 | buildUrl(`https://ghapi.huchen.dev/repositories`, params)
131 | );
132 | if (status !== 200) {
133 | throw new Error('Something went wrong');
134 | }
135 | return data;
136 | }
137 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/components/TopBar.js:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import styled from '@emotion/styled';
3 | import { css, jsx } from '@emotion/core';
4 | import React from 'react';
5 | import PropTypes from 'prop-types';
6 | import { useTheme } from 'emotion-theming';
7 | import LanguageSelect from './LanguageSelect';
8 | import SpokenLanguageSelect from './SpokenLanguageSelect';
9 | import PeriodSelect from './PeriodSelect';
10 | import { ReactComponent as Logo } from '../images/logo.svg';
11 |
12 | const SelectItem = ({ title, children, width }) => {
13 | const theme = useTheme();
14 | return (
15 |
26 | {title && (
27 |
34 | {title}:
35 |
36 | )}
37 |
42 | {children}
43 |
44 |
45 | );
46 | };
47 |
48 | const TopBar = ({
49 | onChangeLanguage,
50 | selectedLanguage,
51 | onChangePeriod,
52 | selectedPeriod,
53 | onChangeSpokenLanguage,
54 | selectedSpokenLanguage,
55 | }) => {
56 | return (
57 |
58 |
109 |
110 | );
111 | };
112 |
113 | TopBar.propTypes = {
114 | selectedLanguage: PropTypes.string,
115 | selectedPeriod: PropTypes.string,
116 | selectedSpokenLanguage: PropTypes.string,
117 | onChangeLanguage: PropTypes.func.isRequired,
118 | onChangePeriod: PropTypes.func.isRequired,
119 | onChangeSpokenLanguage: PropTypes.func.isRequired,
120 | };
121 |
122 | export default React.memo(TopBar);
123 |
124 | const Container = styled.div`
125 | display: flex;
126 | align-items: center;
127 | position: relative;
128 | background-color: ${(props) => props.theme.topBar.bg};
129 | padding: 0 16px;
130 | height: 56px;
131 | box-shadow: 0px 4px 5px 0px rgba(0, 0, 0, 0.14),
132 | 0px 1px 10px 0px rgba(0, 0, 0, 0.12);
133 | transition: background-color ${(props) => props.theme.transition};
134 | `;
135 |
--------------------------------------------------------------------------------
/src/hooks.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo } from 'react';
2 | import { useQuery } from 'react-query';
3 | import useLocalStorage from './hooks/useLocalStorage';
4 |
5 | import {
6 | allLanguagesValue,
7 | allSpokenLanguagesValue,
8 | fetchRepositories,
9 | isEmptyList,
10 | } from './helpers/github';
11 |
12 | import {
13 | KEY_REPOSITORIES,
14 | KEY_LAST_UPDATED,
15 | KEY_SELECTED_CODE_LANGUAGE,
16 | KEY_SELECTED_PERIOD,
17 | KEY_SELECTED_SPOKEN_LANGUAGE,
18 | KEY_SCHEMA_VERSION,
19 | KEY_DARK_MODE,
20 | CURRENT_SCHEMA_VERSION,
21 | } from './helpers/localStorage';
22 |
23 | const getReposByParams = async (key, params) => fetchRepositories(params);
24 |
25 | export const useRepositories = () => {
26 | const [selectedLanguage, setSelectedLanguage] = useSelectedLanguage();
27 | const [selectedPeriod, setSelectedPeriod] = useSelectedPeriod();
28 | const [
29 | selectedSpokenLanguage,
30 | setSelectedSpokenLanguage,
31 | ] = useSelectedSpokenLanguage();
32 | const [repositories, setRepositories] = useLocalStorage(KEY_REPOSITORIES, []);
33 | const [lastUpdatedTime, setLastUpdatedTime] = useLastUpdatedTime();
34 |
35 | const queryKey = useMemo(
36 | () => [
37 | 'repositories',
38 | {
39 | since: selectedPeriod,
40 | language:
41 | selectedLanguage !== allLanguagesValue ? selectedLanguage : undefined,
42 | spokenLanguageCode:
43 | selectedSpokenLanguage !== allSpokenLanguagesValue
44 | ? selectedSpokenLanguage
45 | : undefined,
46 | },
47 | ],
48 | [selectedPeriod, selectedLanguage, selectedSpokenLanguage]
49 | );
50 |
51 | // eslint-disable-next-line react-hooks/exhaustive-deps
52 | const initialQueryKey = useMemo(() => queryKey, []);
53 | // eslint-disable-next-line react-hooks/exhaustive-deps
54 | const initialRepositories = useMemo(() => repositories, []);
55 |
56 | const { isFetching, isError, data, refetch } = useQuery(
57 | queryKey,
58 | getReposByParams,
59 | {
60 | onSuccess: () => {
61 | setLastUpdatedTime();
62 | },
63 | initialData:
64 | queryKey === initialQueryKey && !isEmptyList(initialRepositories)
65 | ? initialRepositories
66 | : undefined,
67 | }
68 | );
69 |
70 | useEffect(() => {
71 | if (!isEmptyList(data)) {
72 | setRepositories(data);
73 | }
74 | }, [data, setRepositories]);
75 |
76 | const isEmpty = isEmptyList(repositories);
77 |
78 | return {
79 | isEmpty,
80 | isLoading: isFetching,
81 | repositories,
82 | lastUpdatedTime,
83 | isError,
84 | refetch,
85 | selectedLanguage,
86 | selectedPeriod,
87 | selectedSpokenLanguage,
88 | setSelectedLanguage,
89 | setSelectedPeriod,
90 | setSelectedSpokenLanguage,
91 | };
92 | };
93 |
94 | export const useSelectedLanguage = () =>
95 | useLocalStorage(KEY_SELECTED_CODE_LANGUAGE, allLanguagesValue);
96 |
97 | export const useSelectedPeriod = () =>
98 | useLocalStorage(KEY_SELECTED_PERIOD, 'daily');
99 |
100 | export const useSelectedSpokenLanguage = () =>
101 | useLocalStorage(KEY_SELECTED_SPOKEN_LANGUAGE, allSpokenLanguagesValue);
102 |
103 | export const useDarkMode = () => {
104 | const preferDarkQuery = '(prefers-color-scheme: dark)';
105 | const [mode, setMode] = useLocalStorage(
106 | KEY_DARK_MODE,
107 | Boolean(window.matchMedia(preferDarkQuery).matches)
108 | );
109 | useEffect(() => {
110 | const mediaQuery = window.matchMedia(preferDarkQuery);
111 | const handleChange = () => setMode(Boolean(mediaQuery.matches));
112 | mediaQuery.addListener(handleChange);
113 | return () => mediaQuery.removeListener(handleChange);
114 | }, [setMode]);
115 |
116 | return [mode, setMode];
117 | };
118 |
119 | export const useLastUpdatedTime = () => {
120 | const [lastUpdatedTime, setLastUpdatedTime] = useLocalStorage(
121 | KEY_LAST_UPDATED
122 | );
123 | function update() {
124 | const newTime = new Date();
125 | setLastUpdatedTime(newTime.getTime());
126 | }
127 | return [lastUpdatedTime, update];
128 | };
129 |
130 | export const useCheckLocalStorageSchema = () => {
131 | const [schemaVersion, setSchemaVersion] = useLocalStorage(KEY_SCHEMA_VERSION);
132 | if (schemaVersion !== CURRENT_SCHEMA_VERSION) {
133 | window.localStorage.clear();
134 | setSchemaVersion(CURRENT_SCHEMA_VERSION);
135 | }
136 | };
137 |
--------------------------------------------------------------------------------
/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 |
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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/images/hero.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------