├── .editorconfig
├── .env
├── .eslintrc
├── .github
└── FUNDING.yml
├── .gitignore
├── design
├── head-image.xcf
└── logo-top-star.ink.svg
├── jsconfig.json
├── license.md
├── package-lock.json
├── package.json
├── promote
├── head-image-440x280.png
├── head-image-marquee.png
├── head-image.png
└── hitup-producthunt-head-image.png
├── public
├── background.js
├── ga.js
├── img
│ ├── icon128.png
│ ├── icon16.png
│ ├── icon48.png
│ └── logo.svg
├── index.html
├── manifest.json
└── whether-occupy-newtab.js
├── readme.md
├── screenshots
├── dark-theme-trending-repo-grid.png
├── filter-by-lang.png
├── not-occupy-ntp.png
├── open-sidebar-menu.png
├── select-color-theme.png
├── toggle-dark-theme.png
├── trending-repo-grid.png
└── trending-repo-list.png
├── scripts
├── build-chrome.sh
├── crop-1280x800-screenshot.sh
├── generate-github-languages-spec.js
├── generate-github-spoken-languages-spec.js
├── generate-multiple-size-logos.js
└── mark-version.sh
├── src
├── app.js
├── app.test.js
├── components
│ ├── alert
│ │ └── index.js
│ ├── built-by-members
│ │ ├── index.js
│ │ └── styles.scss
│ ├── github-ranking
│ │ ├── index.js
│ │ ├── repo-list.js
│ │ └── styles.module.scss
│ ├── github-trending
│ │ ├── index.js
│ │ └── styles.scss
│ ├── language-filter
│ │ └── index.js
│ ├── launcher
│ │ ├── index.js
│ │ └── styles.css
│ ├── loader
│ │ ├── index.js
│ │ └── styles.css
│ ├── ranking-filters
│ │ ├── index.js
│ │ ├── ranking-period-filter
│ │ │ ├── index.js
│ │ │ └── styles.module.scss
│ │ └── styles.module.css
│ ├── repository-grid
│ │ ├── grid-item
│ │ │ ├── index.js
│ │ │ └── styles.module.scss
│ │ └── index.js
│ ├── repository-list
│ │ ├── index.js
│ │ ├── list-item
│ │ │ ├── index.js
│ │ │ └── styles.module.scss
│ │ └── styles.module.scss
│ ├── search-select
│ │ ├── index.js
│ │ └── styles.scss
│ ├── sidebar
│ │ ├── index.js
│ │ ├── sidebar-toggle-bus.js
│ │ └── styles.scss
│ ├── simple-select
│ │ └── index.js
│ ├── spoken-language-filter
│ │ └── index.js
│ ├── top-nav
│ │ ├── index.js
│ │ └── styles.scss
│ ├── top-tip
│ │ ├── index.js
│ │ └── styles.scss
│ ├── trending-filters
│ │ ├── index.js
│ │ ├── styles.module.css
│ │ └── trending-period-filter
│ │ │ └── index.js
│ └── view-filter
│ │ ├── index.js
│ │ └── styles.scss
├── containers
│ ├── comments
│ │ └── index.js
│ ├── feed
│ │ ├── feed.test.js
│ │ ├── index.js
│ │ └── styles.module.scss
│ └── options
│ │ ├── index.js
│ │ └── styles.css
├── custom-bootstrap.scss
├── global.scss
├── icons
│ ├── bars-solid.svg
│ ├── calendar.svg
│ ├── chrome-brands.svg
│ ├── code.svg
│ ├── comments-solid.svg
│ ├── filter-solid.svg
│ ├── fork.js
│ ├── github-brands.svg
│ ├── heart-solid.svg
│ ├── list.svg
│ ├── period.svg
│ ├── spoken-language.svg
│ ├── star.js
│ ├── sun.js
│ ├── table.svg
│ ├── toggle-theme.js
│ ├── twitter-brands.svg
│ └── watcher.js
├── index.js
├── lib
│ ├── date-period.js
│ ├── functools.js
│ ├── functools.test.js
│ ├── gh-repo-search
│ │ ├── index.js
│ │ └── query-tmpl.js
│ ├── gh-trending
│ │ └── index.js
│ ├── github
│ │ ├── index.js
│ │ ├── languages.json
│ │ └── spoken-languages.json
│ ├── runtime.js
│ └── storages.js
├── redux
│ ├── accounts
│ │ ├── actions.js
│ │ ├── reducer.js
│ │ └── types.js
│ ├── preference
│ │ ├── actions.js
│ │ ├── reducer.js
│ │ └── types.js
│ ├── reducers.js
│ └── user-data
│ │ ├── actions.js
│ │ ├── reducer.js
│ │ └── types.js
├── routes
│ ├── index.js
│ └── with-tracker.js
├── store.js
├── theme.scss
└── variables.scss
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | PORT=2001
2 | BROWSER=none
3 | INLINE_RUNTIME_CHUNK=false
4 | REACT_APP_TOP_TIP_URL="https://raw.githubusercontent.com/wonderbeyond/HitUP/user-tips/user-tips.toml"
5 | REACT_APP_GEMMY_BASE_URL="https://wonderbeyond.github.io/gemmy"
6 | REACT_APP_VERSION=4.11.3
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "react-app"
3 | }
4 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: wonderbeyond # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
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: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | Thumbs.db
2 | .DS_Store
3 | .idea
4 | .vscode
5 | /node_modules
6 | /coverage
7 | /build
8 | /old
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 |
13 | .env.local
14 | tags
15 |
--------------------------------------------------------------------------------
/design/head-image.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderbeyond/HitUP/765d99332b9fc14df6e2d2554a33bcb0901d8696/design/head-image.xcf
--------------------------------------------------------------------------------
/design/logo-top-star.ink.svg:
--------------------------------------------------------------------------------
1 |
2 |
19 |
39 |
41 |
43 |
47 |
51 |
52 |
54 |
58 |
62 |
63 |
66 |
70 |
71 |
81 |
89 |
92 |
100 |
101 |
102 |
104 |
105 |
107 | image/svg+xml
108 |
110 |
111 |
112 |
113 |
114 |
124 |
127 |
133 |
139 |
140 |
141 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src/"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Kamran Ahmed
4 |
5 | Copyright (c) 2019 wonderbeyond
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hitup",
3 | "version": "4.11.3",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.18.0",
7 | "bootstrap": "^4.1.3",
8 | "classnames": "^2.2.6",
9 | "compare-versions": "^3.4.0",
10 | "date-fns": "^1.30.1",
11 | "disqus-react": "^1.0.5",
12 | "gemmy-client": "0.0.4",
13 | "lscache": "^1.3.0",
14 | "node-sass": "^4.12.0",
15 | "prop-types": "^15.6.2",
16 | "react": "^16.8.6",
17 | "react-burger-menu": "^2.6.10",
18 | "react-click-outside": "tj/react-click-outside",
19 | "react-dom": "^16.8.6",
20 | "react-ga": "^2.5.7",
21 | "react-redux": "^7.0.3",
22 | "react-router": "^5.0.0",
23 | "react-router-dom": "^5.0.0",
24 | "react-scripts": "3.0.1",
25 | "react-toggle": "^4.0.2",
26 | "react-tooltip": "^3.10.0",
27 | "reactstrap": "^8.0.0",
28 | "redux": "^4.0.1",
29 | "redux-devtools-extension": "^2.13.5",
30 | "redux-persist": "^5.10.0",
31 | "redux-persist-chrome-storage": "^1.0.1",
32 | "redux-thunk": "^2.3.0",
33 | "snarkdown": "^1.2.2",
34 | "tinycache": "^1.1.2",
35 | "toml": "^3.0.0"
36 | },
37 | "scripts": {
38 | "start": "react-scripts start",
39 | "build-chrome": "bash ./scripts/build-chrome.sh",
40 | "test": "react-scripts test --env=jsdom",
41 | "eject": "react-scripts eject",
42 | "gh-pages": "yarn gh-pages:build && echo hitup.wondertools.top > build/CNAME && yarn gh-pages:release",
43 | "gh-pages:build": "cross-env PUBLIC_URL=/ react-scripts build",
44 | "gh-pages:release": "NODE_DEBUG=gh-pages gh-pages -d build"
45 | },
46 | "devDependencies": {
47 | "@testing-library/react": "^8.0.1",
48 | "cross-env": "^5.2.0",
49 | "gh-pages": "^2.0.1",
50 | "jest-dom": "^3.4.0",
51 | "sharp": "^0.22.1",
52 | "svgo": "^1.3.2"
53 | },
54 | "browserslist": [
55 | ">0.2%",
56 | "not dead",
57 | "not ie <= 11",
58 | "not op_mini all"
59 | ]
60 | }
61 |
--------------------------------------------------------------------------------
/promote/head-image-440x280.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderbeyond/HitUP/765d99332b9fc14df6e2d2554a33bcb0901d8696/promote/head-image-440x280.png
--------------------------------------------------------------------------------
/promote/head-image-marquee.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderbeyond/HitUP/765d99332b9fc14df6e2d2554a33bcb0901d8696/promote/head-image-marquee.png
--------------------------------------------------------------------------------
/promote/head-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderbeyond/HitUP/765d99332b9fc14df6e2d2554a33bcb0901d8696/promote/head-image.png
--------------------------------------------------------------------------------
/promote/hitup-producthunt-head-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderbeyond/HitUP/765d99332b9fc14df6e2d2554a33bcb0901d8696/promote/hitup-producthunt-head-image.png
--------------------------------------------------------------------------------
/public/background.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | console.log('HitUP background.js enabled.');
3 | chrome.browserAction.onClicked.addListener(function(tab) {
4 | console.log('Icon clicked.');
5 | chrome.tabs.create({url: 'index.html?ref=BrowserIcon'})
6 | })
7 | })()
8 |
--------------------------------------------------------------------------------
/public/ga.js:
--------------------------------------------------------------------------------
1 | // see https://www.shanebart.com/chrome-ext-analytics/
2 | // for about how to make analytics.js work with chrome extension
3 |
4 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
5 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
6 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
7 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
8 |
9 | ga('create', 'UA-134825122-1', 'auto');
10 | ga('set', 'checkProtocolTask', function(){}); // // Removes failing protocol check.
11 | // ga('send', 'pageview'); // we send pv in react-router hook now
12 |
13 | var appVer = "4.11.3";
14 | ga('set', 'dimension1', appVer);
15 |
--------------------------------------------------------------------------------
/public/img/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderbeyond/HitUP/765d99332b9fc14df6e2d2554a33bcb0901d8696/public/img/icon128.png
--------------------------------------------------------------------------------
/public/img/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderbeyond/HitUP/765d99332b9fc14df6e2d2554a33bcb0901d8696/public/img/icon16.png
--------------------------------------------------------------------------------
/public/img/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderbeyond/HitUP/765d99332b9fc14df6e2d2554a33bcb0901d8696/public/img/icon48.png
--------------------------------------------------------------------------------
/public/img/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | HitUP – Find Top Things
12 |
13 |
14 |
15 |
16 | You need to enable JavaScript to run this app.
17 |
18 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "version": "4.11.3",
4 | "name": "HitUP - Find Top Things",
5 | "short_name": "HitUP",
6 | "description": "Find top things in new tab page, e.g. trending GitHub repositories and more...",
7 | "homepage_url": "https://github.com/wonderbeyond/hitup",
8 | "icons": {
9 | "16": "/img/icon16.png",
10 | "48": "/img/icon48.png",
11 | "128": "/img/icon128.png"
12 | },
13 | "permissions": [
14 | "tabs",
15 | "storage"
16 | ],
17 | "chrome_url_overrides": {
18 | "newtab": "index.html?as-ntp"
19 | },
20 | "browser_action": {
21 | "default_title": "HitUP\nFind Top Things (in new tab)",
22 | "default_icon": "/img/icon128.png"
23 | },
24 | "background": {
25 | "scripts": ["background.js"],
26 | "persistent": false
27 | },
28 | "content_security_policy": "script-src 'self' https://*.googletagmanager.com https://*.google-analytics.com https://disqus.com https://*.disqus.com https://*.disquscdn.com; object-src 'self'"
29 | }
30 |
--------------------------------------------------------------------------------
/public/whether-occupy-newtab.js:
--------------------------------------------------------------------------------
1 | // if user prefer let HitUP not occupy New Tab
2 | (function(store) {
3 | if (document.location.search.indexOf('as-ntp') < 0) {
4 | return
5 | } // else must running as extension
6 | chrome.storage.sync.get('persist:hitup:root', (p) => {
7 | try {
8 | let parsed = JSON.parse(p['persist:hitup:root']);
9 | let preference = JSON.parse(parsed['preference']);
10 | if (!preference.whether_occupy_newtab) {
11 | window.chrome.tabs.update({ url: "chrome-search://local-ntp/local-ntp.html" })
12 | }
13 | } catch (error) {
14 | console.log('ignored error:', error);
15 | }
16 | })
17 | })()
18 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | HitUP
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Find top things in New Tab, including GitHub Trending Repositories
23 |
24 |
25 |
26 | ✨ You can either use HitUP as Chrome extension or just online
27 |
28 |
29 |
30 |
31 | HitUP is a react app and a browser extension that helps you
32 | explore top things in New Tab, including the most popular projects on GitHub
33 |
34 |
35 |
36 |
37 |
38 | ▲ Awesome! HitUP have Dark Theme now 🎉 🎉 🎉
39 |
40 |
41 |
42 |
43 |
44 | ▲ Trending Repositories This Week – Grid View
45 |
46 |
47 |
48 |
49 | ▲ Trending Repositories This Week – List View
50 |
51 |
52 |
53 |
54 | ▲ Filter by Language
55 |
56 |
57 |
58 | ## Installation
59 |
60 | * Chrome Extension – https://chrome.google.com/webstore/detail/hitup-find-top-things/eiokaohkigpbonodjcbjpecbnccijkjb?utm_source=GitHub&utm_medium=wonderbeyond/HitUP
61 | * Online Web – https://hitup.wondertools.top
62 |
63 | ## Supporting HitUP
64 |
65 | If you like HitUP, you can give a star ⭐ on [GitHub](https://github.com/wonderbeyond/HitUP),
66 | or help spread HitUP to more people you know.
67 |
68 | If you love HitUP and hope that I can make it better, you can support me by becoming a backer or sponsor.
69 | Your avatar or logo will show up here with a link to your website.
70 |
71 | [Support HitUP on Patreon](https://www.patreon.com/wonderbeyond)
72 |
73 |
74 | ## Thanks
75 |
76 | * [kamranahmedse/githunt](https://github.com/kamranahmedse/githunt):
77 | HitUP started as a fork of GitHunt, then I totaly changed the data logic to present the real trending repositories.
78 | * HitUP is using [huchenme/github-trending-api](https://github.com/huchenme/github-trending-api)
79 |
80 | ## License
81 |
82 | This project is licensed under the terms of the [MIT license](https://github.com/wonderbeyond/HitUP/blob/master/license.md).
83 |
--------------------------------------------------------------------------------
/screenshots/dark-theme-trending-repo-grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderbeyond/HitUP/765d99332b9fc14df6e2d2554a33bcb0901d8696/screenshots/dark-theme-trending-repo-grid.png
--------------------------------------------------------------------------------
/screenshots/filter-by-lang.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderbeyond/HitUP/765d99332b9fc14df6e2d2554a33bcb0901d8696/screenshots/filter-by-lang.png
--------------------------------------------------------------------------------
/screenshots/not-occupy-ntp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderbeyond/HitUP/765d99332b9fc14df6e2d2554a33bcb0901d8696/screenshots/not-occupy-ntp.png
--------------------------------------------------------------------------------
/screenshots/open-sidebar-menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderbeyond/HitUP/765d99332b9fc14df6e2d2554a33bcb0901d8696/screenshots/open-sidebar-menu.png
--------------------------------------------------------------------------------
/screenshots/select-color-theme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderbeyond/HitUP/765d99332b9fc14df6e2d2554a33bcb0901d8696/screenshots/select-color-theme.png
--------------------------------------------------------------------------------
/screenshots/toggle-dark-theme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderbeyond/HitUP/765d99332b9fc14df6e2d2554a33bcb0901d8696/screenshots/toggle-dark-theme.png
--------------------------------------------------------------------------------
/screenshots/trending-repo-grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderbeyond/HitUP/765d99332b9fc14df6e2d2554a33bcb0901d8696/screenshots/trending-repo-grid.png
--------------------------------------------------------------------------------
/screenshots/trending-repo-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderbeyond/HitUP/765d99332b9fc14df6e2d2554a33bcb0901d8696/screenshots/trending-repo-list.png
--------------------------------------------------------------------------------
/scripts/build-chrome.sh:
--------------------------------------------------------------------------------
1 | set -eux
2 |
3 | export GENERATE_SOURCEMAP=false
4 | react-scripts build
5 |
6 | cd build && zip -r hitup.zip .
7 |
--------------------------------------------------------------------------------
/scripts/crop-1280x800-screenshot.sh:
--------------------------------------------------------------------------------
1 | orig_file=$1
2 | echo "Cropping 1280x800 from $orig_file ..."
3 | orig_width=$(identify -format '%w' ${orig_file})
4 | diff_half=$(( ($orig_width - 1280)/2 ))
5 | convert $orig_file -crop 1280x800+${diff_half}+0 $orig_file
6 |
--------------------------------------------------------------------------------
/scripts/generate-github-languages-spec.js:
--------------------------------------------------------------------------------
1 | const API = 'https://github-trending-api-wonder.herokuapp.com/languages'
2 | const axios = require('axios');
3 |
4 | const popularNames = [
5 | 'Python',
6 | 'C',
7 | 'C++',
8 | 'Java',
9 | 'JavaScript',
10 | 'Ruby',
11 | 'PHP',
12 | 'HTML',
13 | 'Rust',
14 | 'Go',
15 | 'Shell',
16 | ];
17 |
18 | (async function () {
19 | try {
20 | let resp = await axios.get(API)
21 | // console.log('Popular languages:', popularNames);
22 |
23 | let popular = resp.data.filter(l => popularNames.includes(l.name)).sort(
24 | (l, r) => popularNames.indexOf(l.name) - popularNames.indexOf(r.name)
25 | )
26 | let nonPopular = resp.data.filter(l => !popularNames.includes(l.name))
27 |
28 | let languagesINHitUPSpec = popular.concat(nonPopular).map(l => ({
29 | name: l.name,
30 | value: l.urlParam
31 | }))
32 | languagesINHitUPSpec.unshift({
33 | "name": "All Languages",
34 | "value": ""
35 | })
36 | console.log(JSON.stringify(languagesINHitUPSpec, null, 2));
37 | } catch (e) {
38 | console.error(`Failed to request ${API}:`, e)
39 | }
40 | })()
41 |
--------------------------------------------------------------------------------
/scripts/generate-github-spoken-languages-spec.js:
--------------------------------------------------------------------------------
1 | const API = 'https://github-trending-api-wonder.herokuapp.com/spoken_languages'
2 | const axios = require('axios');
3 |
4 | const popularLangs = [
5 | 'en',
6 | 'zh',
7 | 'ru',
8 | 'de',
9 | 'fr',
10 | 'ja',
11 | ];
12 |
13 | (async function () {
14 | try {
15 | let resp = await axios.get(API)
16 |
17 | let popular = resp.data.filter(l => popularLangs.includes(l.urlParam)).sort(
18 | (l, r) => popularLangs.indexOf(l.urlParam) - popularLangs.indexOf(r.urlParam)
19 | )
20 | let nonPopular = resp.data.filter(l => !popularLangs.includes(l.urlParam))
21 |
22 | let languagesINHitUPSpec = popular.concat(nonPopular).map(l => ({
23 | name: l.name,
24 | value: l.urlParam
25 | }))
26 | languagesINHitUPSpec.unshift({
27 | "name": "All Languages",
28 | "value": ""
29 | })
30 | console.log(JSON.stringify(languagesINHitUPSpec, null, 2));
31 | } catch (e) {
32 | console.error(`Failed to request ${API}:`, e)
33 | }
34 | })()
35 |
--------------------------------------------------------------------------------
/scripts/generate-multiple-size-logos.js:
--------------------------------------------------------------------------------
1 | const { exec } = require('child_process');
2 | const sharp = require('sharp');
3 |
4 | var inPic = 'design/logo-top-star.ink.svg';
5 | var outDir = 'public/img';
6 | var sizes = [128, 48, 16];
7 |
8 | var trans = sharp(inPic);
9 | var fs = sizes.map(size => {
10 | let destPath = `${outDir}/icon${size}.png`
11 | return trans
12 | .resize(size, size)
13 | .toFile(destPath)
14 | .then(() => {
15 | console.info(`generated a copy of ${size}x${size}: ${destPath}`);
16 | })
17 | });
18 |
19 | Promise.all(fs).then(() => {
20 | console.info('multiple-sizes completed');
21 | })
22 |
23 | var optSVGCommand = `svgo --pretty --indent=2 -i ${inPic} -o ${outDir}/logo.svg`;
24 | exec(optSVGCommand, (error, stdout, stderr) => {
25 | console.log(optSVGCommand);
26 |
27 | if (error) {
28 | console.error(`exec error: ${error}`);
29 | return;
30 | }
31 | console.info(`done optimized svg logo: ${stdout}`)
32 | if (stderr) {
33 | console.error(`stderr: ${stderr}`);
34 | }
35 | })
36 |
--------------------------------------------------------------------------------
/scripts/mark-version.sh:
--------------------------------------------------------------------------------
1 | set -eu
2 |
3 | version=$1
4 |
5 | sed -i -re "s/REACT_APP_VERSION=.+/REACT_APP_VERSION=${version}/" .env
6 | sed -i -re "s/\"version\": \".+\"/\"version\": \"${version}\"/" package.json
7 | sed -i -re "s/\"version\": \".+\"/\"version\": \"${version}\"/" public/manifest.json
8 | sed -i -re "s/var appVer = \".+\"/var appVer = \"${version}\"/" public/ga.js
9 |
10 | echo "After you commit changes, don't forget: git tag ${version}"
11 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Provider } from 'react-redux';
3 | import { connect } from 'react-redux';
4 | import { PersistGate } from 'redux-persist/lib/integration/react';
5 | import { HashRouter } from 'react-router-dom';
6 | import TopNav from 'components/top-nav';
7 |
8 | import Launcher from './components/launcher';
9 | import { persist, store } from './store';
10 | import AppRoutes from './routes';
11 | import TopTip from 'components/top-tip';
12 | import SideBar from "components/sidebar";
13 |
14 | class App extends Component {
15 | constructor(props) {
16 | super(props);
17 | this.state = {
18 | isSideBarOpen: false
19 | };
20 | }
21 |
22 | render() {
23 | return (
24 |
25 | } persistor={ persist }>
26 |
27 |
28 |
29 | );
30 | }
31 | }
32 |
33 | const PageWrapper = props => (
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | )
42 |
43 |
44 | const ThemeWrapper = connect(store => ({
45 | theme: store.preference.theme,
46 | }))(props => (
47 |
51 | ))
52 |
53 | export default App;
54 |
--------------------------------------------------------------------------------
/src/app.test.js:
--------------------------------------------------------------------------------
1 | import ReactGA from 'react-ga';
2 | import React from 'react';
3 | import App from './app';
4 |
5 | import {
6 | render,
7 | // fireEvent,
8 | cleanup,
9 | waitForElement,
10 | } from '@testing-library/react'
11 | import 'jest-dom/extend-expect'
12 |
13 | afterEach(cleanup)
14 |
15 | test('Render default trending repo list', async () => {
16 | ReactGA.initialize('foo', { testMode: true });
17 | const {getAllByText} = render( );
18 | const bbms = await waitForElement(() => getAllByText('Built by'));
19 | expect(bbms.length).toBeGreaterThan(10);
20 | });
21 |
--------------------------------------------------------------------------------
/src/components/alert/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import classnames from 'classnames';
4 |
5 | const Alert = (props) => (
6 |
7 | { props.heading &&
{props.heading} }
8 | { props.children }
9 |
10 | );
11 |
12 | Alert.propTypes = {
13 | type: PropTypes.string.isRequired
14 | };
15 |
16 | export default Alert;
17 |
--------------------------------------------------------------------------------
/src/components/built-by-members/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | // import {UncontrolledTooltip} from 'reactstrap';
4 | import ReactTooltip from 'react-tooltip'
5 |
6 |
7 | import './styles.scss';
8 |
9 | class BuiltByMembers extends React.Component {
10 | state = {
11 | readyForTooltipImgLoading: false
12 | };
13 |
14 | componentDidCatch(error, info) {
15 | this.setState({ hasError: true });
16 | console.log('Error in BuiltByMembers:')
17 | console.error(error, info);
18 | }
19 |
20 | render() {
21 | return this.setState({ readyForTooltipImgLoading: true })}>
22 | {this.props.members.map(m => {
23 | let targetID = `tooltip:${this.props.repository.author}/${this.props.repository.name}@${m.username}`;
24 | return (
25 |
26 |
28 |
29 |
30 | {
36 | if (!this.state.readyForTooltipImgLoading) { return null }
37 | return <>
38 |
39 | {m.username}
40 | >
41 | }}>
42 |
43 |
44 | )
45 | })}
46 |
47 | }
48 | }
49 |
50 | BuiltByMembers.propTypes = {
51 | members: PropTypes.array.isRequired,
52 | repository: PropTypes.object.isRequired
53 | };
54 |
55 | export default BuiltByMembers;
56 |
--------------------------------------------------------------------------------
/src/components/built-by-members/styles.scss:
--------------------------------------------------------------------------------
1 | @import "theme.scss";
2 |
3 | .built-by-member-item {
4 | &:first-of-type {
5 | margin-left: 4px;
6 | }
7 | &:not(:last-of-type) {
8 | margin-right: 2px;
9 | }
10 |
11 | .built-by-member-avatar img {
12 | background-color: white;
13 | width: 20px;
14 | height: 20px;
15 | display: inline-block;
16 | border-radius: 50%;
17 | vertical-align: middle;
18 | }
19 | .built-by-member-avatar:hover img {
20 | box-shadow: 0px 2px 3px var(--box-shadow-color);
21 | }
22 |
23 | .built-by-member-popup {
24 | opacity: 1!important;
25 | background-color: rgba(0, 0, 0, 0.9)!important;
26 | padding: 10px 10px 0 10px;
27 | text-align: center;
28 | img {
29 | background-color: white;
30 | width: 100px;
31 | height: 100px;
32 | border-radius: 8px;
33 | display: block;
34 | margin: auto;
35 | }
36 | .username {
37 | display: inline-block;
38 | font-weight: bold;
39 | height: 30px;
40 | line-height: 30px;
41 | font-size: 14px;
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/github-ranking/index.js:
--------------------------------------------------------------------------------
1 | import ReactGA from 'react-ga';
2 | import React, { useState, useEffect } from 'react';
3 | import Alert from 'components/alert';
4 | import Loader from 'components/loader';
5 | import { connect } from 'react-redux';
6 | import {
7 | updateGitHubAccessToken
8 | } from 'redux/accounts/actions';
9 |
10 | import RankingFilters from 'components/ranking-filters';
11 | import { fetchTrendingRepositories } from 'lib/gh-trending';
12 | import RepositoryList from './repo-list';
13 | import { trendingPeriodDefs } from 'lib/gh-trending';
14 | import {preferredStorage} from 'lib/storages';
15 | import {prefetch} from 'lib/functools';
16 | import {ReactComponent as GitHub} from 'icons/github-brands.svg';
17 | import styles from './styles.module.scss';
18 | import repoSearch from 'lib/gh-repo-search';
19 |
20 | const persistKey = 'HitUP:preference:GitHubRanking';
21 | const loadPrefernce = prefetch(() => preferredStorage
22 | .getItem(persistKey).then(data => {
23 | if (!data) {throw new Error('No data')}
24 | const parsed = JSON.parse(data);
25 | console.debug(`Loaded preference from ${persistKey}:`, parsed);
26 | return parsed;
27 | })
28 | .catch(reason => {
29 | console.debug('No data from new-style storage')
30 | return null;
31 | }))
32 |
33 |
34 | function usePreference(initial = {}, options = {}) {
35 | /**
36 | * Component-level persistable preference management
37 | *
38 | * I believe not every state is suitable to persist,
39 | * So this hook helps you maintain the part of persistable-
40 | * states (i.e. preference) independently.
41 | **/
42 |
43 | initial._rehydrated = false;
44 | const {persistKey} = options;
45 | if (!persistKey) {throw new Error('persistKey required')}
46 | const [state, update] = useState(initial);
47 | // const [storageChecked, setStorageChecked] = useState(false);
48 |
49 | // for loading preference from storage
50 | if (!state._rehydrated) {
51 | loadPrefernce().then(pref => {
52 | update({...state, ...pref, _rehydrated: true});
53 | })
54 | }
55 |
56 | function setPreference(item, value) {
57 | const newState = { ...state, [item]: value };
58 | update(newState);
59 | preferredStorage.setItem(persistKey, JSON.stringify(newState));
60 | ReactGA.event({
61 | category: 'Preference',
62 | label: `Set Ranking ${item}`,
63 | action: `Ranking ${item} Set to ${JSON.stringify(value)}`
64 | });
65 | }
66 | return { preference: state, setPreference }
67 | }
68 |
69 | function GitHubRanking(props) {
70 | const [state, setState] = useState({
71 | processing: true,
72 | repositories: [],
73 | error: null,
74 | after: null,
75 | });
76 |
77 | const { preference, setPreference } = usePreference({
78 | // viewType: 'grid',
79 |
80 | createdPeriod: {spec: 'Last 3 Days'},
81 | language: '',
82 | qwords: '',
83 | fork: true,
84 | first: 30,
85 | searchIn: ['name', 'description'],
86 | topic: null,
87 | license: null,
88 |
89 | sort: 'stars-desc',
90 | }, {persistKey});
91 |
92 | console.log('state.after:', state.after);
93 | console.log('preference:', preference);
94 | useEffect(
95 | loadRepos, [
96 | props.GitHubAccessToken,
97 | preference,
98 | state.after,
99 | ]
100 | );
101 |
102 | function loadRepos() {
103 | if (!preference._rehydrated) {
104 | return
105 | }
106 |
107 | if (!state.processing) {
108 | // to ignore rerendering by wrap the if-condition test
109 | setState({
110 | ...state,
111 | processing: true,
112 | repositories: [],
113 | error: null
114 | });
115 | }
116 |
117 | props.GitHubAccessToken && repoSearch({
118 | accessToken: props.GitHubAccessToken,
119 | qwords: preference.qwords,
120 | fork: preference.fork,
121 | first: preference.first,
122 | after: state.after,
123 | createdPeriod: preference.createdPeriod,
124 | searchIn: preference.searchIn,
125 | language: preference.language,
126 | topic: preference.topic,
127 | license: preference.license,
128 | sort: preference.sort,
129 | }).then(result => {
130 | const count = result.data.search.repositoryCount;
131 | const repositories = result.data.search.nodes;
132 |
133 | setState({
134 | ...state,
135 | processing: false,
136 | repositories,
137 | error: null
138 | });
139 |
140 | console.log(`Got ${count} repos:`, repositories);
141 | })
142 | // fetchTrendingRepositories(filters).then(repositories => {
143 | // if (!(repositories && repositories.length)) {
144 | // throw new Error("Empty List")
145 | // }
146 |
147 | // setState({
148 | // processing: false,
149 | // repositories,
150 | // error: null
151 | // });
152 | // }).catch(error => {
153 | // let message = error.response &&
154 | // error.response.data &&
155 | // error.response.data.message;
156 |
157 | // if (!message) {
158 | // message = error.message;
159 | // }
160 |
161 | // setState({
162 | // processing: false,
163 | // repositories: [],
164 | // error: message
165 | // })
166 | // });
167 | }
168 |
169 | function getCorrespondingGitHubLink() {
170 | // Returns official trending link
171 | return "https://github.com/trending/" + preference.language +
172 | "?since=" +
173 | trendingPeriodDefs[preference.since].ghParamKey;
174 | }
175 |
176 | function renderErrors() {
177 | if (!state.error) {
178 | return null;
179 | }
180 |
181 | let message = '';
182 | switch (state.error.toLowerCase()) {
183 | case 'empty list':
184 | message = (
185 |
186 | Trending repositories results are currently being dissected.
187 | This may be a few minutes.
188 | You can visit GitHub's trending page instead.
189 |
190 | );
191 | break;
192 | default:
193 | message = state.error;
194 | break;
195 | }
196 |
197 | return (
198 |
199 | {message}
200 |
201 | );
202 | }
203 |
204 | function hasRepositories() {
205 | return state.repositories && state.repositories.length !== 0;
206 | }
207 |
208 | // pop access_token from url
209 | const cUrl = new URL(document.location.href);
210 | if (cUrl.searchParams.has('access_token')) {
211 | props.updateGitHubAccessToken(cUrl.searchParams.get('access_token'));
212 | cUrl.searchParams.delete('access_token');
213 | cUrl.searchParams.delete('oauth2_provider');
214 | cUrl.searchParams.delete('token_type');
215 | window.history.pushState({}, '', cUrl.href);
216 | }
217 |
218 | // display login button if no access_token
219 | if (!props.GitHubAccessToken) {
220 | function goOAuth2Flow() {
221 | const authUrl = `${process.env.REACT_APP_OAUTH2_MUX_SERVER}/github/authorize?redirect_uri=` + encodeURIComponent(document.location.href);
222 | document.location.href = authUrl;
223 | }
224 | return (
225 | <>
226 |
227 |
Login is required to get advanced GitHub ranking.
228 |
229 |
230 | Login GitHub
231 |
232 |
233 |
234 |
Or you can
235 | generate a token and paste below.
236 |
props.updateGitHubAccessToken(e.target.value)}
237 | />
238 |
239 | >
240 | )
241 | }
242 |
243 |
244 | return (
245 | (preference._rehydrated) && <>
246 |
247 |
248 | GitHub Advanced Ranking
249 |
250 |
251 |
252 |
setPreference('language', l)}
256 | // updateViewType={vt => setPreference('viewType', vt)}
257 | updateDatePeriod={sc => setPreference('createdPeriod', sc)}
258 | selectedDatePeriod={preference.createdPeriod}
259 | />
260 |
261 |
262 | {hasRepositories() && }
266 | {state.processing && }
267 |
268 | {!state.processing && !hasRepositories() && renderErrors()}
269 | >
270 | );
271 | }
272 |
273 | const mapStateToProps = store => {
274 | return {
275 | GitHubAccessToken: store.accounts.GitHubAccessToken,
276 | };
277 | };
278 |
279 | const mapDispatchToProps = {
280 | updateGitHubAccessToken
281 | }
282 |
283 | export default connect(mapStateToProps, mapDispatchToProps)(GitHubRanking);
284 |
--------------------------------------------------------------------------------
/src/components/github-ranking/repo-list.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | // import ReactTooltip from 'react-tooltip';
4 | import Star, {Stars} from 'icons/star';
5 | import Fork from 'icons/fork';
6 | import BuiltByMembers from "components/built-by-members";
7 |
8 | import styles from './styles.module.scss';
9 |
10 | class ListItem extends React.Component {
11 | render() {
12 | let itemKey = `${this.props.repository.owner.login}/${this.props.repository.name}`;
13 | let periodStarsTargetID = `${itemKey}:period-stars`;
14 |
15 | return (
16 |
74 | );
75 | }
76 | }
77 |
78 | ListItem.propTypes = {
79 | repository: PropTypes.object.isRequired
80 | };
81 |
82 |
83 | export default function RepositoryList(props) {
84 | return (
85 |
86 | {
87 | props.repositories.map(repository => )
88 | }
89 |
90 | );
91 | }
92 |
93 | RepositoryList.propTypes = {
94 | repositories: PropTypes.array.isRequired,
95 | };
96 |
--------------------------------------------------------------------------------
/src/components/github-ranking/styles.module.scss:
--------------------------------------------------------------------------------
1 | .headerRow {
2 | display: flex;
3 | align-items: center;
4 | justify-content: space-between;
5 | flex-wrap: wrap;
6 | margin-top: -1em;
7 |
8 | & > div {
9 | margin-top: 1em;
10 | }
11 | .groupHeading {
12 | .textCapitalizes {
13 | font-size: 1.35rem;
14 | white-space: nowrap;
15 | }
16 | }
17 | }
18 |
19 | .loginGitHubTip, .pasteTokenTip {
20 | margin: 2rem 0;
21 | text-align: center;
22 | .tokenInput {
23 | padding: .25em .5em;
24 | width: 20em;
25 | border: 1px solid var(--border-primary-color);
26 | border-radius: 3px;
27 | box-shadow: none;
28 | outline: 0;
29 | }
30 | }
31 |
32 | .repoListContainer {
33 | background: var(--background-secondary-color);
34 | }
35 |
36 | .listItemContainer {
37 | &:last-child {
38 | margin-bottom: 0;
39 | border-bottom: 0;
40 | padding-bottom: 0;
41 | }
42 |
43 | padding: 0 0 20px;
44 | margin-bottom: 20px;
45 | border-bottom: 1px solid var(--border-primary-color);
46 | position: relative;
47 |
48 | .repoHeader h3 {
49 | margin-bottom: 4px;
50 | font-size: 20px;
51 | font-weight: 700;
52 | }
53 |
54 | .repoHeader h3 .textNormal {
55 | font-weight: 400;
56 | }
57 |
58 | .repoHeader p {
59 | margin-bottom: 0;
60 | }
61 |
62 | .repoHeader,
63 | .repo-body {
64 | margin-bottom: 15px;
65 | }
66 |
67 | .repo-body {
68 | color: var(--text-main-color);
69 | p {
70 | margin-bottom: 0;
71 | display: block;
72 | max-width: 80%;
73 | }
74 | }
75 |
76 | .repo-footer {
77 | color: var(--text-secondary-color);
78 | font-size: 13px;
79 | a {
80 | color: var(--text-secondary-color);
81 | text-decoration: none;
82 | }
83 | .repo-language-color {
84 | border-radius: 50%;
85 | display: inline-block;
86 | height: 12px;
87 | position: relative;
88 | top: 1px;
89 | width: 12px;
90 | }
91 | svg {
92 | position: relative;
93 | top: -1px;
94 | margin-right: 6px;
95 | fill: var(--text-secondary-color);
96 | vertical-align: text-top;
97 |
98 | }
99 | }
100 |
101 | .repoMeta a {
102 | color: currentColor;
103 | text-decoration: none;
104 | font-weight: 500;
105 | }
106 |
107 | .author-link {
108 | position: absolute;
109 | top: 15px;
110 | right: 15px;
111 | }
112 |
113 | .author-img {
114 | height: 92px;
115 | width: 92px;
116 | border-radius: 10px;
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/components/github-trending/index.js:
--------------------------------------------------------------------------------
1 | import ReactGA from 'react-ga';
2 | import React, { useState, useEffect } from 'react';
3 | import Alert from 'components/alert';
4 | import Loader from 'components/loader';
5 | import TrendingFilters from 'components/trending-filters';
6 | import { fetchTrendingRepositories } from 'lib/gh-trending';
7 | import RepositoryList from 'components/repository-list';
8 | import RepositoryGrid from 'components/repository-grid';
9 | import { trendingPeriodDefs } from 'lib/gh-trending';
10 | import {preferredStorage} from 'lib/storages';
11 | import {prefetch} from 'lib/functools';
12 | import './styles.scss';
13 |
14 | const persistKey = 'HitUP:preference:GitHubTrending';
15 | const loadPrefernce = prefetch(() => preferredStorage
16 | .getItem(persistKey).then(data => {
17 | if (!data) {throw new Error('No data')}
18 | const parsed = JSON.parse(data);
19 | console.debug(`Loaded preference from ${persistKey}:`, parsed);
20 | return parsed;
21 | })
22 | .catch(reason => {
23 | console.debug('No data from new-style storage')
24 | return preferredStorage.getItem('persist:hitup:root').then(data => {
25 | // Migrate old preference data from redux-persist
26 | const globalPref = JSON.parse(JSON.parse(data).preference);
27 | return {
28 | language: globalPref.language,
29 | since: globalPref.dateJump,
30 | viewType: globalPref.viewType,
31 | }
32 | }).catch(reason => {
33 | console.debug('No data at all (ignored, maybe new user).');
34 | });
35 | }))
36 |
37 |
38 | function usePreference(initial = {}, options = {}) {
39 | /**
40 | * Component-level persistable preference management
41 | *
42 | * I believe not every state is suitable to persist,
43 | * So this hook helps you maintain the part of persistable-
44 | * states (i.e. preference) independently.
45 | **/
46 |
47 | initial._rehydrated = false;
48 | const {persistKey} = options;
49 | if (!persistKey) {throw new Error('persistKey required')}
50 | const [state, update] = useState(initial);
51 | // const [storageChecked, setStorageChecked] = useState(false);
52 |
53 | // for loading preference from storage
54 | if (!state._rehydrated) {
55 | loadPrefernce().then(pref => {
56 | update({...state, ...pref, _rehydrated: true});
57 | })
58 | }
59 |
60 | function setPreference(item, value) {
61 | const newState = { ...state, [item]: value };
62 | update(newState);
63 | preferredStorage.setItem(persistKey, JSON.stringify(newState));
64 | ReactGA.event({
65 | category: 'Preference',
66 | label: `Set Trending ${item}`,
67 | action: `Trending ${item} Set to ${JSON.stringify(value)}`
68 | });
69 | }
70 | return { preference: state, setPreference }
71 | }
72 |
73 | function GitHubTrending(props) {
74 | const [state, setState] = useState({
75 | processing: true,
76 | repositories: [],
77 | error: null,
78 | });
79 |
80 | const { preference, setPreference } = usePreference({
81 | language: '',
82 | since: 'week',
83 | viewType: 'grid',
84 | }, {persistKey});
85 |
86 | // load trending data when:
87 | // - persisted preference done rehydrated
88 | // - language or since changeed
89 | useEffect(
90 | loadTrendingRepositories,
91 | [preference._rehydrated, preference.spokenLanguage, preference.language, preference.since]
92 | );
93 |
94 | function loadTrendingRepositories() {
95 | if (!preference._rehydrated) {
96 | return
97 | }
98 | const filters = {
99 | 'spokenLanguage': preference.spokenLanguage,
100 | 'language': preference.language,
101 | 'dateJump': preference.since,
102 | };
103 |
104 | if (!state.processing) {
105 | // to ignore rerendering by wrap the if-condition test
106 | setState({
107 | processing: true,
108 | repositories: [],
109 | error: null
110 | });
111 | }
112 |
113 | fetchTrendingRepositories(filters).then(repositories => {
114 | if (!(repositories && repositories.length)) {
115 | throw new Error("Empty List")
116 | }
117 |
118 | setState({
119 | processing: false,
120 | repositories,
121 | error: null
122 | });
123 | }).catch(error => {
124 | let message = error.response &&
125 | error.response.data &&
126 | error.response.data.message;
127 |
128 | if (!message) {
129 | message = error.message;
130 | }
131 |
132 | setState({
133 | processing: false,
134 | repositories: [],
135 | error: message
136 | })
137 | });
138 | }
139 |
140 | function getCorrespondingGitHubLink() {
141 | // Returns official trending link
142 | return "https://github.com/trending/" + preference.language +
143 | "?since=" +
144 | trendingPeriodDefs[preference.since].ghParamKey;
145 | }
146 |
147 | function renderErrors() {
148 | if (!state.error) {
149 | return null;
150 | }
151 |
152 | let message = '';
153 | switch (state.error.toLowerCase()) {
154 | case 'empty list':
155 | message = (
156 |
157 | Trending repositories results are currently being dissected.
158 | This may be a few minutes.
159 | You can visit GitHub's trending page instead.
160 |
161 | );
162 | break;
163 | default:
164 | message = state.error;
165 | break;
166 | }
167 |
168 | return (
169 |
170 | {message}
171 |
172 | );
173 | }
174 |
175 | function hasRepositories() {
176 | return state.repositories && state.repositories.length !== 0;
177 | }
178 |
179 | return (
180 | preference._rehydrated && <>
181 |
182 |
183 | GitHub Trending Repos
184 |
185 | {trendingPeriodDefs[preference.since].heading.toLocaleLowerCase()}
186 |
187 |
188 |
189 |
setPreference('language', l)}
194 | updateSpokenLanguage={l => setPreference('spokenLanguage', l)}
195 | updateViewType={vt => setPreference('viewType', vt)}
196 | updateDateJump={sc => setPreference('since', sc)}
197 | selectedDateJump={preference.since}
198 | />
199 |
200 |
201 | {hasRepositories() && (
202 | preference.viewType === 'grid' ? :
209 | )}
210 | {state.processing && }
211 |
212 | {!state.processing && !hasRepositories() && renderErrors()}
213 | >
214 | );
215 | }
216 |
217 | export default GitHubTrending;
218 |
--------------------------------------------------------------------------------
/src/components/github-trending/styles.scss:
--------------------------------------------------------------------------------
1 | .header-row {
2 | display: flex;
3 | align-items: center;
4 | justify-content: space-between;
5 | flex-wrap: wrap;
6 | margin-top: -1em;
7 |
8 | & > div {
9 | margin-top: 1em;
10 | }
11 | .group-heading {
12 | .text-capitalizes {
13 | font-size: 1.35rem;
14 | white-space: nowrap;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/language-filter/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from "prop-types";
3 | import { languages } from "lib/github";
4 | import SelectSearch from "components/search-select";
5 | import {ReactComponent as CodeIcon} from 'icons/code.svg';
6 |
7 | function LanguageFilter(props) {
8 | return (
9 | }
16 | />
17 | );
18 | }
19 |
20 | LanguageFilter.propTypes = {
21 | updateLanguage: PropTypes.func.isRequired,
22 | selectedLanguage: PropTypes.string
23 | };
24 |
25 | export default LanguageFilter;
26 |
--------------------------------------------------------------------------------
/src/components/launcher/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import './styles.css';
4 |
5 | const Launcher = () => (
6 |
12 | );
13 |
14 | export default Launcher;
15 |
--------------------------------------------------------------------------------
/src/components/launcher/styles.css:
--------------------------------------------------------------------------------
1 | @keyframes fadein {
2 | from {
3 | opacity: 0.1;
4 | }
5 | to {
6 | opacity: 1;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/loader/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import './styles.css';
4 | import Sun from 'icons/sun';
5 |
6 | const Loader = () => (
7 |
8 |
9 |
10 | );
11 |
12 | export default Loader;
13 |
--------------------------------------------------------------------------------
/src/components/loader/styles.css:
--------------------------------------------------------------------------------
1 | .loading-indicator {
2 | margin: 50px 0 0;
3 | text-align: center;
4 | }
5 |
6 | .loading-indicator svg {
7 | height: 85px;
8 | width: 85px;
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/ranking-filters/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import LanguageFilter from 'components/language-filter';
5 | import ViewFilter from 'components/view-filter';
6 | import styles from './styles.module.css';
7 | import RankingPeriodFilter from './ranking-period-filter';
8 |
9 | const RankingFilters = (props) => (
10 |
11 |
12 |
16 |
17 |
18 |
22 |
23 | {/*
24 |
28 |
*/}
29 |
30 | );
31 |
32 | RankingFilters.propTypes = {
33 | updateLanguage: PropTypes.func.isRequired,
34 | // updateViewType: PropTypes.func.isRequired,
35 | updateDatePeriod: PropTypes.func.isRequired,
36 | selectedLanguage: PropTypes.string,
37 | selectedViewType: PropTypes.string,
38 | selectedDatePeriod: PropTypes.object
39 | };
40 |
41 | export default RankingFilters;
42 |
--------------------------------------------------------------------------------
/src/components/ranking-filters/ranking-period-filter/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {useState} from 'react';
3 | import PropTypes from 'prop-types';
4 | import { ReactComponent as PeriodIcon } from 'icons/period.svg';
5 | import {realizePeriod} from 'lib/date-period';
6 | import classNames from 'classnames';
7 | import styles from './styles.module.scss';
8 | import {format} from 'date-fns';
9 | import ClickOutside from 'react-click-outside';
10 | // eslint-disable-next-line
11 | function toDateStr(d) {
12 | // only leave the date part
13 | if (!d) {return ''}
14 | if (typeof d === 'string') {
15 | return d.slice(0, 10)
16 | }
17 | return d.toISOString().slice(0, 10)
18 | }
19 |
20 | function formatPeriod(period) {
21 | if (period.spec) {
22 | return period.spec
23 | }
24 | return [format(period.start, 'YYYY-MM-DD'), format(period.end, 'YYYY-MM-DD')].join('~')
25 | }
26 |
27 | const RankingPeriodFilter = (props) => {
28 | let initPeriod;
29 | try {
30 | initPeriod = realizePeriod(props.value);
31 | } catch (e) {
32 | initPeriod = realizePeriod({spec: 'Last 3 Days'})
33 | }
34 | const [showDropdown, setShowDropdown] = useState(false);
35 | const [period, setPeriod] = useState({
36 | start: initPeriod.start,
37 | end: initPeriod.end,
38 | spec: initPeriod.spec,
39 | });
40 | // eslint-disable-next-line
41 | const changeHandler = event => {
42 | const newPeriod = {
43 | ...period,
44 | [event.target.name]: event.target.value,
45 | spec: undefined,
46 | }
47 | setPeriod(newPeriod)
48 | props.onChange(newPeriod)
49 | }
50 |
51 | const quickSetPeriod = spec => () => {
52 | setPeriod(realizePeriod({spec}))
53 | props.onChange({spec})
54 | setShowDropdown(false)
55 | }
56 |
57 | return setShowDropdown(false)}>
58 |
59 |
setShowDropdown(!showDropdown)}>
60 |
61 | {formatPeriod(period)}
62 |
63 | {showDropdown &&
64 | {/*
Created in this period
65 |
66 | Start Date
67 |
68 |
69 |
70 | End Date
71 |
72 | */}
73 |
74 | Past Day
75 | Past Week
76 | Past Month
77 | Past Year
78 | Last 3 Days
79 | Last 3 Weeks
80 | Last 3 Months
81 | Last 3 Years
82 | Total History
83 |
84 |
}
85 |
86 |
87 | }
88 |
89 | RankingPeriodFilter.propTypes = {
90 | onChange: PropTypes.func.isRequired,
91 | value: PropTypes.object.isRequired
92 | };
93 |
94 | export default RankingPeriodFilter;
95 |
--------------------------------------------------------------------------------
/src/components/ranking-filters/ranking-period-filter/styles.module.scss:
--------------------------------------------------------------------------------
1 | @import '~bootstrap/scss/functions';
2 | @import '~bootstrap/scss/variables';
3 |
4 | .wrapper {
5 | position: relative;
6 | }
7 | .dropDown {
8 | z-index: 1;
9 | position: absolute;
10 | top: 100%;
11 | // @media (max-width: 575.98px) {
12 | right: 0;
13 | // }
14 | min-width: 300px;
15 | margin-top: 5px;
16 | overflow: hidden;
17 | font-size: 12px;
18 | color: #586069;
19 | background-color: #fff;
20 | border: 1px solid #e8e8e8;
21 | border-radius: 8px;
22 | box-shadow: 0 3px 12px rgba(27, 31, 35, 0.15);
23 |
24 | h5 {
25 | color: #24292e;
26 | }
27 |
28 | label {
29 | display: inline-flex;
30 | flex-direction: column;
31 | &:not(:last-of-type) {
32 | margin: 0 1rem 0 0;
33 | }
34 | width: 45%;
35 |
36 | span {
37 | cursor: pointer;
38 | }
39 | input {
40 | border: 1px solid #dfe2e5;
41 | border-radius: 3px;
42 | box-shadow: none;
43 | outline: 0;
44 | &::-webkit-inner-spin-button {
45 | display: none;
46 | }
47 | &::-webkit-clear-button {
48 | display: none;
49 | }
50 | &::-webkit-calendar-picker-indicator {
51 | background: transparent;
52 | }
53 | }
54 | }
55 |
56 | .descriptivePeriods {
57 | display: flex;
58 | flex-wrap: wrap;
59 | justify-content: space-between;
60 | button {
61 | font-size: 12px;
62 | display: inline-block;
63 | text-align: center;
64 | background: $blue;
65 | color: #FFF;
66 | padding: 3px 0;
67 | margin-top: 3px;
68 | width: 33%;
69 | border-radius: 8px;
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/ranking-filters/styles.module.css:
--------------------------------------------------------------------------------
1 | .filtersWrap {
2 | display: flex;
3 | }
4 | .filterItem:not(:first-child) {
5 | margin-left: 1em;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/repository-grid/grid-item/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import styles from './styles.module.scss';
5 |
6 | import ReactTooltip from 'react-tooltip';
7 | import {trendingPeriodDefs} from 'lib/gh-trending';
8 | import Star, {Stars} from 'icons/star';
9 | import Fork from 'icons/fork';
10 |
11 | // import {UncontrolledTooltip} from 'reactstrap';
12 | import BuiltByMembers from "components/built-by-members";
13 |
14 | class GridItem extends React.Component {
15 | render() {
16 | let itemKey = `${this.props.repository.owner.login}/${this.props.repository.name}`;
17 | let periodStarsTargetID = `${itemKey}:period-stars`;
18 |
19 | return (
20 |
21 |
22 |
37 |
38 |
43 |
44 | {this.props.repository.languageColor &&
45 |
46 | { this.props.repository.language }
47 | }
48 | {this.props.repository.builtBy && this.props.repository.builtBy.length > 0 && Built by
49 |
50 | }
51 |
52 |
53 |
54 |
{ (this.props.repository.description && this.props.repository.description.slice(0, 140)) || 'No description given.' }
55 |
56 |
78 |
79 |
80 | );
81 | }
82 | }
83 |
84 | GridItem.propTypes = {
85 | repository: PropTypes.object.isRequired
86 | };
87 |
88 | export default GridItem;
89 |
--------------------------------------------------------------------------------
/src/components/repository-grid/grid-item/styles.module.scss:
--------------------------------------------------------------------------------
1 | .grid-item-container {
2 | padding: 10px;
3 | transition: all 0.3s ease;
4 | }
5 |
6 | .grid-item-body {
7 | background: var(--background-secondary-color);
8 | padding: 20px;
9 | border-radius: 10px;
10 | box-shadow: -5px 10px 60px -13px rgba(0, 0, 0, 0.2);
11 | height: 270px;
12 | }
13 |
14 | .grid-item-container .author-details {
15 | margin-bottom: 20px;
16 | }
17 |
18 | .grid-item-container .author-img img {
19 | background-color: white;
20 | width: 35px;
21 | height: 35px;
22 | object-fit: cover;
23 | border-radius: 5px;
24 | float: left;
25 | margin-right: 15px;
26 | }
27 |
28 | .grid-item-container .author-details h5 {
29 | margin-bottom: 2px;
30 | font-weight: 600;
31 | color: var(--text-secondary-color);
32 | font-size: 14px;
33 | }
34 |
35 | .grid-item-container .author-details p {
36 | margin-bottom: 0;
37 | font-size: 12px;
38 | }
39 |
40 | .grid-item-container {
41 | a {
42 | text-decoration: none;
43 | }
44 | .author-header,
45 | .repo-header,
46 | .repo-body {
47 | // margin-bottom: 15px;
48 | font-size: 14px;
49 | color: var(--text-main-color);
50 | }
51 |
52 | .author-header {
53 | margin-bottom: 15px;
54 | }
55 |
56 | .repo-header {
57 | margin-bottom: 8px;
58 | overflow: hidden;
59 |
60 | h5 {
61 | font-size: 19px;
62 | margin-bottom: 5px;
63 | .repo-name {
64 | font-weight: 700;
65 | white-space: nowrap;
66 | text-overflow: ellipsis;
67 | }
68 | }
69 |
70 | .repo-meta {
71 | a {
72 | color: currentColor;
73 | font-weight: 500;
74 | }
75 |
76 | height: 24px;
77 | line-height: 24px;
78 | display: flex;
79 | align-items: center;
80 | white-space: nowrap;
81 | .repo-language-color {
82 | border-radius: 50%;
83 | display: inline-block;
84 | height: 12px;
85 | position: relative;
86 | top: 1px;
87 | width: 12px;
88 | }
89 | }
90 | }
91 |
92 | .repo-body {
93 | margin-bottom: 0;
94 | $line-height: 1.5em;
95 | max-height: $line-height * 4;
96 | overflow: hidden;
97 | p {
98 | line-height: $line-height;
99 | margin: 0;
100 | }
101 | }
102 |
103 | .repo-footer {
104 | font-size: 12px;
105 | position: absolute;
106 | bottom: 20px;
107 | left: 20px;
108 | right: 25px;
109 | // background: white;
110 | padding: 10px;
111 | a {
112 | text-decoration: none;
113 | color: var(--text-secondary-color);
114 | font-weight: 500;
115 | }
116 | svg {
117 | position: relative;
118 | top: -2px;
119 | fill: var(--text-secondary-color);
120 | margin-right: 5px;
121 | }
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/components/repository-grid/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import GridItem from './grid-item';
4 |
5 | class RepositoryGrid extends React.Component {
6 | render() {
7 | return (
8 |
9 | { this.props.repositories.map(repository => ) }
10 |
11 | );
12 | }
13 | }
14 |
15 | RepositoryGrid.propTypes = {
16 | repositories: PropTypes.array.isRequired,
17 | dateJump: PropTypes.string.isRequired
18 | };
19 |
20 | export default RepositoryGrid;
21 |
--------------------------------------------------------------------------------
/src/components/repository-list/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import styles from './styles.module.scss';
5 | import ListItem from './list-item';
6 |
7 | export default function RepositoryList(props) {
8 | return (
9 |
10 | {
11 | props.repositories.map(repository => )
12 | }
13 |
14 | );
15 | }
16 |
17 | RepositoryList.propTypes = {
18 | repositories: PropTypes.array.isRequired,
19 | dateJump: PropTypes.string.isRequired
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/repository-list/list-item/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import styles from './styles.module.scss';
5 |
6 | import ReactTooltip from 'react-tooltip';
7 | import {trendingPeriodDefs} from 'lib/gh-trending';
8 | import Star, {Stars} from 'icons/star';
9 | import Fork from 'icons/fork';
10 | import BuiltByMembers from "components/built-by-members";
11 |
12 | class ListItem extends React.Component {
13 | render() {
14 | let itemKey = `${this.props.repository.owner.login}/${this.props.repository.name}`;
15 | let periodStarsTargetID = `${itemKey}:period-stars`;
16 |
17 | return (
18 |
78 | );
79 | }
80 | }
81 |
82 | ListItem.propTypes = {
83 | repository: PropTypes.object.isRequired
84 | };
85 |
86 | export default ListItem;
87 |
--------------------------------------------------------------------------------
/src/components/repository-list/list-item/styles.module.scss:
--------------------------------------------------------------------------------
1 | @import "theme.scss";
2 |
3 | .list-item-container {
4 | &:last-child {
5 | margin-bottom: 0;
6 | border-bottom: 0;
7 | padding-bottom: 0;
8 | }
9 |
10 | padding: 0 0 20px;
11 | margin-bottom: 20px;
12 | border-bottom: 1px solid var(--border-primary-color);
13 | position: relative;
14 |
15 | .repo-header h3 {
16 | margin-bottom: 4px;
17 | font-size: 20px;
18 | font-weight: 700;
19 | }
20 |
21 | .repo-header h3 .text-normal {
22 | font-weight: 400;
23 | }
24 |
25 | .repo-header p {
26 | margin-bottom: 0;
27 | }
28 |
29 | .repo-header,
30 | .repo-body {
31 | margin-bottom: 15px;
32 | }
33 |
34 | .repo-body {
35 | color: var(--text-main-color);
36 | p {
37 | margin-bottom: 0;
38 | display: block;
39 | max-width: 80%;
40 | }
41 | }
42 |
43 | .repo-footer {
44 | color: var(--text-secondary-color);
45 | font-size: 13px;
46 | a {
47 | color: var(--text-secondary-color);
48 | text-decoration: none;
49 | }
50 | .repo-language-color {
51 | border-radius: 50%;
52 | display: inline-block;
53 | height: 12px;
54 | position: relative;
55 | top: 1px;
56 | width: 12px;
57 | }
58 | svg {
59 | position: relative;
60 | top: -1px;
61 | margin-right: 6px;
62 | fill: var(--text-secondary-color);
63 | vertical-align: text-top;
64 |
65 | }
66 | }
67 |
68 | .repo-meta a {
69 | color: currentColor;
70 | text-decoration: none;
71 | font-weight: 500;
72 | }
73 |
74 | .author-link {
75 | position: absolute;
76 | top: 15px;
77 | right: 15px;
78 | }
79 |
80 | .author-img {
81 | background-color: white;
82 | height: 92px;
83 | width: 92px;
84 | border-radius: 92px;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/components/repository-list/styles.module.scss:
--------------------------------------------------------------------------------
1 | .list-container {
2 | background: var(--background-secondary-color);
3 | border-radius: 8px;
4 | box-shadow: -5px 10px 60px -13px rgba(0, 0, 0, 0.20);
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/search-select/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import classNames from "classnames";
4 | import ClickOutside from "react-click-outside";
5 |
6 | import "./styles.scss";
7 | import { isMobileDevice } from "lib/runtime";
8 |
9 | class SelectSearch extends React.Component {
10 | constructor(props) {
11 | console.log("SelectSearch:constructor:", props);
12 | super(props);
13 | this.itemsMap = new Map(props.options.map(item => [item.value, item.name]));
14 | const filteredItems = [...props.options.map(item => item.value)];
15 | this.state = {
16 | value: props.value,
17 | filterText: "",
18 | filteredItems: filteredItems,
19 | focusedIndex: filteredItems.indexOf(props.value),
20 | showDropdown: false
21 | };
22 | }
23 |
24 | filterInputRef = React.createRef();
25 |
26 | focusFilterInput = () => {
27 | this.filterInputRef.current.focus();
28 | };
29 |
30 | componentDidUpdate(prevProps, prevState) {
31 | if (prevState.filterText !== this.state.filterText) {
32 | const filteredItems = this.getFilteredItems();
33 | this.setState({
34 | filteredItems: filteredItems,
35 | focusedIndex: filteredItems.indexOf(this.state.value)
36 | });
37 | }
38 |
39 | setTimeout(() => {
40 | this.ensureFocusedVisible();
41 | }, 0);
42 |
43 | if (
44 | this.state.showDropdown &&
45 | !prevState.showDropdown &&
46 | !isMobileDevice()
47 | ) {
48 | this.focusFilterInput();
49 | }
50 | }
51 |
52 | ensureFocusedVisible() {
53 | if (!this.focusedItem) {
54 | return;
55 | }
56 |
57 | const domNode = ReactDOM.findDOMNode(this.focusedItem);
58 | if (!domNode) {
59 | return;
60 | }
61 |
62 | domNode.scrollIntoView({
63 | behavior: "auto",
64 | block: "nearest"
65 | });
66 | }
67 |
68 | getFilteredItems() {
69 | let availableItems;
70 |
71 | if (this.state.filterText) {
72 | availableItems = this.props.options.filter(item => {
73 | const itemText = item.name.toLowerCase();
74 | const selectedText = this.state.filterText.toLowerCase();
75 |
76 | return itemText.indexOf(selectedText) >= 0;
77 | });
78 | } else {
79 | availableItems = [...this.props.options];
80 | }
81 |
82 | return availableItems.map(item => item.value);
83 | }
84 |
85 | renderOptions() {
86 | return this.state.filteredItems.map((itemKey, counter) => {
87 | const displayName = this.itemsMap.get(itemKey);
88 | const isSelectedIndex = this.state.value === itemKey;
89 | const isFocused = this.state.focusedIndex === counter;
90 |
91 | return (
92 | {
98 | if (isSelectedIndex) {
99 | this.activeItem = element;
100 | }
101 | if (isFocused) {
102 | this.focusedItem = element;
103 | }
104 | }}
105 | onClick={() => this.selectItem(counter)}
106 | key={counter}
107 | >
108 | {displayName}
109 |
110 | );
111 | });
112 | }
113 |
114 | onKeyDown = e => {
115 | const { focusedIndex } = this.state;
116 |
117 | const isEnterKey = e.keyCode === 13;
118 | const isUpKey = e.keyCode === 38;
119 | const isDownKey = e.keyCode === 40;
120 |
121 | if (!isUpKey && !isDownKey && !isEnterKey) {
122 | return;
123 | }
124 |
125 | e.preventDefault();
126 |
127 | // arrow up/down button should select next/previous list element
128 | if (isUpKey && focusedIndex > 0) {
129 | this.setState(prevState => ({
130 | focusedIndex: prevState.focusedIndex - 1
131 | }));
132 | } else if (
133 | isDownKey &&
134 | focusedIndex < this.state.filteredItems.length - 1
135 | ) {
136 | this.setState(prevState => ({
137 | focusedIndex: prevState.focusedIndex + 1
138 | }));
139 | } else if (isEnterKey && this.state.filteredItems[focusedIndex]) {
140 | this.selectItem(focusedIndex);
141 | }
142 | };
143 |
144 | selectItem = index => {
145 | const selectedItem = this.state.filteredItems[index];
146 | if (selectedItem === undefined) {
147 | return;
148 | }
149 |
150 | this.setState({
151 | focusedIndex: index,
152 | value: selectedItem,
153 | filterText: "",
154 | showDropdown: false
155 | });
156 |
157 | this.props.onChange(selectedItem);
158 | };
159 |
160 | hideDropdown = () => {
161 | this.setState({
162 | showDropdown: false,
163 | filterText: ""
164 | });
165 | };
166 |
167 | filterTextChangeHandler = e => {
168 | this.setState({
169 | filterText: e.target.value,
170 | focusedIndex: -1
171 | });
172 | };
173 |
174 | getOptionDropdown() {
175 | return (
176 |
177 |
{this.props.title}
178 |
190 |
{this.renderOptions()}
191 |
192 | );
193 | }
194 |
195 | toggleDropdown = () => {
196 | this.setState(prevState => ({
197 | showDropdown: !prevState.showDropdown
198 | }));
199 | };
200 |
201 | DefaultIconComponent (state) {}
202 |
203 | render() {
204 | const IconComponent = this.props.IconComponent || this.DefaultIconComponent;
205 |
206 | return (
207 |
208 |
209 |
213 | {IconComponent(this.state)}
214 | {this.itemsMap.get(this.state.value)}
215 |
216 | {this.state.showDropdown && this.getOptionDropdown()}
217 |
218 |
219 | );
220 | }
221 | }
222 |
223 | export default SelectSearch;
224 |
--------------------------------------------------------------------------------
/src/components/search-select/styles.scss:
--------------------------------------------------------------------------------
1 | @import "theme.scss";
2 |
3 | .select-search-wrap {
4 | position: relative;
5 | z-index: 1;
6 | }
7 |
8 | .search-select-btn {
9 | border-radius: 8px;
10 | padding: 10px 20px;
11 | cursor: default;
12 | // display: block;
13 | }
14 |
15 | .search-select {
16 | position: absolute;
17 | top: 100%;
18 | width: 300px;
19 | margin-top: 5px;
20 | overflow: hidden;
21 | font-size: 12px;
22 | color: #586069;
23 | background-color: #fff;
24 | background-clip: padding-box;
25 | border: 1px solid #e8e8e8;
26 | border-radius: 8px;
27 | box-shadow: 0 3px 12px rgba(27, 31, 35, 0.15);
28 | }
29 |
30 | .select-menu-title {
31 | padding: 3px 10px;
32 | background: white;
33 | border-bottom: 1px solid #e8e8e8;
34 | font-weight: bold;
35 | }
36 |
37 | .select-menu-filters {
38 | background-color: #f4f4f4;
39 | }
40 |
41 | .select-menu-text-filter {
42 | padding: 10px;
43 | border-bottom: 1px solid #e8e8e8;
44 |
45 | & input {
46 | display: block;
47 | font-size: 14px;
48 | width: 100%;
49 | max-width: 100%;
50 | border: 1px solid #dfe2e5 !important;
51 | padding: 5px 10px !important;
52 | border-radius: 3px;
53 | box-shadow: none !important;
54 | outline: none !important;
55 | }
56 | }
57 |
58 | .select-menu-list {
59 | position: relative;
60 | max-height: 400px;
61 | overflow: auto;
62 | }
63 |
64 | .select-menu-item {
65 | text-align: left;
66 | background-color: #fff;
67 | border-top: 0;
68 | border-right: 0;
69 | border-left: 0;
70 | display: block;
71 | padding: 10px 8px 10px 16px;
72 | overflow: hidden;
73 | color: inherit;
74 | cursor: pointer;
75 | border-bottom: 1px solid #e8e8e8;
76 | }
77 |
78 | .select-menu-list .select-menu-item:last-child {
79 | border-bottom: none;
80 | }
81 |
82 | .select-menu-item {
83 | .select-menu-item-text {
84 | display: block;
85 | text-align: left;
86 | }
87 |
88 | &.active-item,
89 | &.focused-item {
90 | color: #fff;
91 | background-color: #1157ed;
92 | text-decoration: none;
93 | & .select-menu-item-text {
94 | color: #fff;
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/components/sidebar/index.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect} from "react";
2 | import { connect } from 'react-redux';
3 | import { slide as Menu } from "react-burger-menu";
4 | import {
5 | setColorTheme, setWhetherOccupyNewTab
6 | } from 'redux/preference/actions';
7 | import {registerToggleSideBarHandler} from 'components/sidebar/sidebar-toggle-bus';
8 | import Toggle from 'react-toggle';
9 | import {isRunningExtension} from 'lib/runtime';
10 |
11 | import "react-toggle/style.css";
12 | import './styles.scss';
13 |
14 | const SideBar = props => {
15 | const [isOpen, setIsOpen] = useState(false);
16 |
17 | useEffect(() => {
18 | registerToggleSideBarHandler(() => {
19 | setIsOpen(!isOpen)
20 | });
21 | });
22 |
23 | const syncSideBarState = (state) => {
24 | setIsOpen(state.isOpen)
25 | }
26 |
27 | const setThemeHandler = event => {
28 | let selected = event.target.value;
29 | props.setColorTheme(selected);
30 | console.debug('theme set to', selected);
31 | }
32 |
33 | const setONTHandler = event => {
34 | props.setWhetherOccupyNewTab(event.target.checked);
35 | }
36 |
37 | return (
38 |
44 | Settings
45 |
46 |
47 |
48 | Color Theme
49 |
50 | System
51 | Light
52 | Dark
53 | Dark Blue
54 |
55 |
56 |
57 |
58 | {isRunningExtension &&
59 |
60 | Occupy New Tab
61 |
62 |
63 | }
64 |
65 | );
66 | };
67 |
68 | const mapStateToProps = store => {
69 | return {
70 | theme: store.preference.theme,
71 | whether_occupy_newtab: store.preference.whether_occupy_newtab,
72 | };
73 | };
74 |
75 | const mapDispatchToProps = {
76 | setColorTheme,
77 | setWhetherOccupyNewTab
78 | }
79 |
80 | export default connect(mapStateToProps, mapDispatchToProps)(SideBar);
81 |
--------------------------------------------------------------------------------
/src/components/sidebar/sidebar-toggle-bus.js:
--------------------------------------------------------------------------------
1 | const _bus = []; // _bus[0] stores handler to toggle sidebar
2 |
3 | export function registerToggleSideBarHandler(func) {
4 | _bus[0] = func
5 | }
6 |
7 | export function toggleSideBar(func) {
8 | if (_bus[0]) {
9 | _bus[0]()
10 | } else {
11 | console.log('No toggleSideBar handler found!');
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/sidebar/styles.scss:
--------------------------------------------------------------------------------
1 | @import "theme.scss";
2 |
3 | /* Position and sizing of clickable cross button */
4 | .bm-cross-button {
5 | height: 24px;
6 | width: 24px;
7 | }
8 |
9 | /* Color/shape of close button cross */
10 | .bm-cross {
11 | background: var(--text-main-color);
12 | }
13 |
14 | .bm-overlay {
15 | background: rgba(0, 0, 0, 0.3);
16 | }
17 |
18 | .bm-menu-wrap {
19 | position: fixed;
20 | height: 100%;
21 | .bm-menu {
22 | background: var(--background-secondary-color);
23 | padding: 1em 1.5em;
24 | .bm-item-list {
25 | color: var(--text-main-color);
26 | .bm-item {
27 | display: inline-block;
28 | outline: 0;
29 | padding: 3px 0;
30 | label {
31 | display: flex;
32 | align-items: center;
33 | }
34 | select {
35 | background: var(--background-secondary-color);
36 | color: var(--text-main-color);
37 | border: 1px solid var(--border-primary-color);
38 | }
39 | }
40 | h2.bm-item {
41 | font-size: 1.5em;
42 | padding: 0;
43 | margin: 0 0 1em 0;
44 | }
45 | .bm-item.ont-option {
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/simple-select/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
4 |
5 | class SimpleSelect extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | dropdownOpen: false,
10 | };
11 | }
12 |
13 | getSelected() {
14 | for (let i = 0; i < this.props.options.length; i++) {
15 | const option = this.props.options[i];
16 | if (this.props.value === option.value) {
17 | return option
18 | }
19 | }
20 | throw new Error('Invalid option');
21 | }
22 | updateSelected = (option) => {
23 | if (option.value === this.getSelected().value) {
24 | return;
25 | }
26 | this.setState({ selected: option })
27 | this.props.onChange(option.value);
28 | };
29 |
30 | toggle = () => {
31 | this.setState(prevState => ({
32 | dropdownOpen: !prevState.dropdownOpen
33 | }));
34 | };
35 |
36 | render() {
37 | return (
38 |
39 |
40 | {this.props.decor}{this.getSelected().label}
41 |
42 |
43 | {this.props.options.map(option =>
44 | this.updateSelected(option)}>{option.label}
47 | )}
48 |
49 |
50 | );
51 | }
52 | }
53 |
54 | SimpleSelect.propTypes = {
55 | decor: PropTypes.element,
56 | onChange: PropTypes.func.isRequired,
57 | value: PropTypes.string.isRequired,
58 | options: PropTypes.array.isRequired,
59 | };
60 |
61 | export default SimpleSelect;
62 |
--------------------------------------------------------------------------------
/src/components/spoken-language-filter/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from "prop-types";
3 | import { spokenLanguages } from "lib/github";
4 | import SelectSearch from "components/search-select";
5 | import { ReactComponent as SpokenLanguageIcon } from "icons/spoken-language.svg";
6 |
7 | function SpokenLanguageFilter(props) {
8 | return (
9 | }
16 | />
17 | );
18 | }
19 |
20 | SpokenLanguageFilter.propTypes = {
21 | updateLanguage: PropTypes.func.isRequired,
22 | selectedLanguage: PropTypes.string
23 | };
24 |
25 | export default SpokenLanguageFilter;
26 |
--------------------------------------------------------------------------------
/src/components/top-nav/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import ToggleTheme from 'icons/toggle-theme';
4 | import { Link } from 'react-router-dom';
5 | import ReactTooltip from 'react-tooltip'
6 | import {GemmyClient} from 'gemmy-client';
7 | import snarkdown from 'snarkdown';
8 | import { setColorTheme } from 'redux/preference/actions';
9 | import {toggleSideBar} from 'components/sidebar/sidebar-toggle-bus';
10 | import {ReactComponent as Heart} from 'icons/heart-solid.svg';
11 | import {ReactComponent as GitHub} from 'icons/github-brands.svg';
12 | import {ReactComponent as Twitter} from 'icons/twitter-brands.svg';
13 | import {ReactComponent as Chrome} from 'icons/chrome-brands.svg';
14 | import {ReactComponent as Comments} from 'icons/comments-solid.svg';
15 | import {ReactComponent as Bars} from 'icons/bars-solid.svg';
16 |
17 | import './styles.scss';
18 |
19 | class TopNav extends React.Component {
20 | tweet = `HitUP – a Chrome extension help you find top things in New Tab
21 | https://github.com/wonderbeyond/HitUP
22 | `;
23 |
24 | constructor(props) {
25 | super(props);
26 | this.state = {
27 | hitGem: ''
28 | }
29 | }
30 |
31 | componentDidMount() {
32 | setTimeout(async () => {
33 | let gmc = new GemmyClient()
34 | let hitGem = await gmc.randomGet()
35 | this.setState({hitGem: hitGem});
36 | }, 0);
37 | }
38 |
39 | toggleTheme = () => {
40 | let prev = this.props.theme;
41 | let right = (prev === 'light'? 'dark': 'light');
42 | console.log(`Toggling color theme from ${prev} to ${right}`);
43 | this.props.setColorTheme(right);
44 | }
45 |
46 | render() {
47 | // We need that to show the extension button only if not running in extension
48 | const isRunningExtension = window.chrome &&
49 | window.chrome.runtime &&
50 | window.chrome.runtime.id;
51 |
52 | return (
53 |
54 |
55 |
59 |
60 |
61 |
62 |
HitUP
63 |
Find to p things
64 |
65 |
66 |
67 |
68 |
70 |
71 |
72 |
73 |
74 | {
75 | isRunningExtension && (
76 |
81 |
82 |
83 | )
84 | }
85 |
Rate HitUP in Chrome Web Store
86 |
87 |
92 |
93 |
94 |
View source on GitHub
95 |
96 | {
97 | !isRunningExtension && (
98 |
103 |
104 |
105 | )
106 | }
107 |
Get HitUP as Chrome extension
108 |
109 |
114 |
115 |
116 |
117 |
118 |
122 |
123 |
124 |
Let's discuss here
125 |
126 |
127 | {this.state.hitGem &&
128 |
129 |
133 | Powered by wonderbeyond/gemmy .
134 | You are welcome to contribute items. ❤️❤️❤️
135 | Any ideas or questions?
136 |
137 |
}
138 |
139 |
140 | );
141 | }
142 | }
143 |
144 | const mapStateToProps = store => {
145 | return {
146 | theme: store.preference.theme,
147 | };
148 | };
149 |
150 | const mapDispatchToProps = {
151 | setColorTheme
152 | }
153 |
154 | export default connect(mapStateToProps, mapDispatchToProps)(TopNav);
155 |
--------------------------------------------------------------------------------
/src/components/top-nav/styles.scss:
--------------------------------------------------------------------------------
1 | @import "variables.scss";
2 | @import 'theme.scss';
3 |
4 | .top-nav {
5 | background: var(--top-nav-background-color);
6 | border-bottom: 1px solid var(--top-nav-border-color);
7 | padding: 0;
8 | // margin-bottom: 35px;
9 | position: relative;
10 | overflow: hidden;
11 |
12 | .container {
13 | position: relative;
14 | height: 105px;
15 | }
16 |
17 | .logo {
18 | margin-left: -10px;
19 | margin-top: 15px;
20 | img {
21 | width: 64px;
22 | height: 64px;
23 | float: left;
24 | }
25 | }
26 |
27 | .logo-text {
28 | float: left;
29 | margin-left: 10px;
30 | margin-top: 25px;
31 |
32 | h4 {
33 | margin: 3px 0 0;
34 | font-weight: 700;
35 | }
36 |
37 | .top-text {
38 | position: relative;
39 | }
40 | .top-arrow {
41 | display: inline-block;
42 | width: 0;
43 | height: 0;
44 |
45 | position: absolute;
46 | top: -4px;
47 | left: 1px;
48 |
49 | border-top: 4px solid #eee0;
50 | border-left: 4px solid #eee0;
51 | border-right: 4px solid #eee0;
52 | border-bottom: 4px solid #e46773;
53 | }
54 |
55 | p {
56 | margin-bottom: 0;
57 | }
58 | }
59 |
60 | .theme-toggle {
61 | cursor: pointer;
62 | position: absolute;
63 | right: 20px;
64 | top: -10px;
65 | opacity: 0.4;
66 | transition: all 0.3s ease-out;
67 | &:hover {
68 | top: -3px;
69 | opacity: 1;
70 | }
71 | }
72 |
73 | .nav-icon-links {
74 | margin: 25px 0 0 0;
75 |
76 | .nav-link-item {
77 | float: left;
78 | text-align: center;
79 | width: 24px;
80 | height: 24px;
81 | line-height: 24px;
82 | font-size: 20px;
83 | &:not(:first-of-type) {
84 | margin-left: 5px;
85 | }
86 |
87 | color: var(--text-secondary-color);
88 | &:hover {
89 | color: var(--text-main-color);
90 | }
91 |
92 | }
93 | .nav-link-item:hover {
94 | text-decoration: none;
95 | position: relative;
96 | top: -1px;
97 | }
98 | }
99 |
100 | .sidebar-toggle {
101 | margin: 25px 5px 0 1em;
102 | line-height: 24px;
103 | font-size: 18px;
104 | cursor: pointer;
105 | color: var(--text-secondary-color);
106 | &:hover {
107 | color: var(--text-main-color);
108 | font-weight: bold;
109 | }
110 | }
111 |
112 | .gemmy-words {
113 | position: absolute;
114 | right: 20px;
115 | bottom: 0px;
116 | font-size: 13px;
117 | color: var(--text-main-color);
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/components/top-tip/index.js:
--------------------------------------------------------------------------------
1 | import ReactGA from 'react-ga';
2 | import React, { createRef, useState, useEffect } from "react";
3 | import {parse as tomlParse}from 'toml';
4 | import {isRunningExtension, isRunningChromeExtension} from 'lib/runtime';
5 | import { connect } from 'react-redux';
6 | import {dismissUserTip} from 'redux/user-data/actions';
7 | import {CSSTransition} from 'react-transition-group';
8 | import compareVersions from 'compare-versions';
9 |
10 | import './styles.scss';
11 |
12 | const TopTip = props => {
13 | const runtimeEnv = {
14 | version: process.env.REACT_APP_VERSION,
15 | stage: process.env.NODE_ENV,
16 | isMaintainer: document.location.search.indexOf('__as_maintainer') > 0,
17 | mediaWidth: window.innerWidth,
18 | isRunningExtension: isRunningExtension,
19 | isRunningChromeExtension: isRunningChromeExtension,
20 | isRunningPlainWeb: document.location.href.startsWith('http'),
21 | }
22 |
23 | const [activeTip, setActiveTip] = useState(null);
24 | const [inProp, setInProp] = useState(false); // http://reactcommunity.org/react-transition-group/css-transition
25 | const tipRef = createRef();
26 |
27 | const matchDisplayCondition = tip => {
28 | // default returns true, unless there is an unmet condition
29 | // So, only returns false in branch conditon test!
30 |
31 | if (tip.disabled) {return false}
32 |
33 | const envReqs = tip.env_requires;
34 |
35 | if (envReqs.version) {
36 | // match version spec, e.g. ">=4.8.1"
37 | const mat = envReqs.version.match(/^(>=|<=|=|>|<)((\d+\.){2}\d+)$/);
38 | if (!mat) {
39 | console.error(`Invalid version spec: ${envReqs.version}`);
40 | return false;
41 | }
42 | const [op, verReq] = [mat[1], mat[2]];
43 | const cpmRes = compareVersions(runtimeEnv.version, verReq);
44 | const versionMet = (
45 | (op === '>' && cpmRes > 0) ||
46 | (op === '>=' && cpmRes >= 0) ||
47 | (op === '=' && cpmRes === 0) ||
48 | (op === '<=' && cpmRes <= 0) ||
49 | (op === '<' && cpmRes < 0)
50 | );
51 | console.debug(
52 | `Tip#${tip.id} requires(version${op}${verReq}), version=${runtimeEnv.version}, cmpRes: ${cpmRes}, met: ${versionMet}`
53 | );
54 | if (!versionMet) {return false}
55 | }
56 |
57 | if (envReqs.time) {
58 | const mat = envReqs.time.match(/^(>|<)(.+)$/);
59 | if (!mat) {
60 | console.error(`Invalid time spec: ${envReqs.time}`);
61 | return false;
62 | }
63 | const [op, timeReq] = [mat[1], new Date(mat[2])];
64 | const now = new Date()
65 | const timeMet = (
66 | (op === '>' && +now > +timeReq) ||
67 | (op === '<' && +now < +timeReq)
68 | );
69 | console.debug(
70 | `Tip#${tip.id} requires(time${op}${timeReq}), time=${now}, met: ${timeMet}`
71 | );
72 | if (!timeMet) {return false}
73 | }
74 |
75 | if (envReqs.use_time) {
76 | // match how long has current user been using this app
77 | // e.g. ">3 days"
78 | const unit2msec = {
79 | 'seconds': 1000,
80 | 'minutes': 1000*60,
81 | 'hours': 1000*60*60,
82 | 'days': 1000*60*60*24,
83 | }
84 |
85 | // eslint-disable-next-line no-useless-escape
86 | const mat = envReqs.use_time.match(/^(>|<)([\d\.]+) (seconds|minutes|hours|days)/);
87 | const [op, useTimeReq, unit] = [mat[1], mat[2], mat[3]];
88 | const useTimeReqInMsec = parseFloat(useTimeReq) * unit2msec[unit];
89 | const now = new Date();
90 | const firstSeenTime = new Date(props.firstSeenTime);
91 | const realUseTime = now - firstSeenTime;
92 |
93 | const useTimeMet = (
94 | (op === '>' && realUseTime > useTimeReqInMsec) ||
95 | (op === '<' && realUseTime < useTimeReqInMsec)
96 | )
97 |
98 | console.debug(
99 | `Tip#${tip.id} requires(useTime${op}${useTimeReq} ${unit}),` +
100 | ` realUseTime=${realUseTime/unit2msec[unit]} ${unit}, met: ${useTimeMet}`
101 | );
102 |
103 | if (!useTimeMet) {return false}
104 | }
105 |
106 | if (envReqs.stage && envReqs.stage !== runtimeEnv.stage) {
107 | console.debug(`Stage mismatch for Tip#${tip.id}`);
108 | return false;
109 | }
110 |
111 | if (envReqs.is_maintainer && !runtimeEnv.isMaintainer) {
112 | console.debug(`Tip#${tip.id} not to dispaly due to maintainer required`);
113 | return false;
114 | }
115 |
116 | if (envReqs.web_extension && !runtimeEnv.isRunningExtension) {
117 | console.debug(`Tip#${tip.id} not to dispaly due to require running as web extension`);
118 | return false;
119 | }
120 |
121 | if (envReqs.media_min_width && runtimeEnv.mediaWidth < envReqs.media_min_width) {
122 | console.debug(`Tip#${tip.id} not to dispaly due to mediaWidth>=${envReqs.media_min_width} required`);
123 | return false;
124 | }
125 |
126 | return true;
127 | }
128 |
129 | useEffect(() => {
130 | // I only want fetch tips while initial page load
131 | function fetchAndFeed() {
132 | fetch(
133 | process.env.REACT_APP_TOP_TIP_URL
134 | ).then(data => data.text()).then(text => {
135 | let parsed = tomlParse(text);
136 | console.debug(parsed);
137 | parsed.tips.some(tip => {
138 | if (props.dismissedUserTips.includes(tip.id)) {
139 | return false
140 | }
141 | if (matchDisplayCondition(tip)) {
142 | setActiveTip(tip)
143 | setInProp(true);
144 | return true;
145 | }
146 | return false;
147 | });
148 | })
149 | }
150 |
151 | setTimeout(fetchAndFeed, 1000);
152 | // eslint-disable-next-line react-hooks/exhaustive-deps
153 | }, []);
154 |
155 |
156 | useEffect(() => {
157 | if (tipRef.current) {
158 | tipRef.current.addEventListener('click', event => {
159 | if (event.target.matches('a.action')) {
160 | ReactGA.event({
161 | category: 'User Action',
162 | label: 'User Tip',
163 | action: `User Responded Tip#${activeTip.id}`,
164 | });
165 | dismiss();
166 | console.debug(`Closed tip#${activeTip.id} after user take action`)
167 | }
168 | });
169 | }
170 | // eslint-disable-next-line react-hooks/exhaustive-deps
171 | }, [activeTip]);
172 |
173 | const dismiss = () => {
174 | setInProp(false);
175 | props.dismissUserTip(activeTip.id);
176 | setActiveTip(null);
177 | }
178 |
179 | return (
180 |
181 | {(activeTip &&
182 |
183 | ×
184 |
) ||
185 | Mew~
186 |
}
187 |
188 | );
189 | };
190 |
191 | const mapStateToProps = store => {
192 | return {
193 | dismissedUserTips: store.userData.dismissedUserTips,
194 | firstSeenTime: store.userData.firstSeenTime,
195 | };
196 | };
197 |
198 | const mapDispatchToProps = {
199 | dismissUserTip
200 | }
201 |
202 | export default connect(mapStateToProps, mapDispatchToProps)(TopTip);
203 |
--------------------------------------------------------------------------------
/src/components/top-tip/styles.scss:
--------------------------------------------------------------------------------
1 | $background: #2D3E50;
2 | $color: #F4CB89;
3 | $linkColor: #4698F5;
4 |
5 | .top-tip {
6 | text-align: center;
7 | font-size: 14px;
8 | line-height: 20px;
9 | min-height: 36px;
10 | padding: 0.5em;
11 | background: $background;
12 | color: $color;
13 |
14 | justify-content: center;
15 | align-items: center;
16 |
17 | opacity: 0;
18 | display: flex;
19 | transition: all 0.3s ease;
20 |
21 | &-empty {
22 | display: none;
23 | }
24 |
25 | &-enter {
26 | opacity: 0;
27 | }
28 | &-enter-active, &-enter-done {
29 | opacity: 1;
30 | }
31 |
32 | &-exit {
33 | opacity: 1;
34 | }
35 | &-exit-active {
36 | opacity: 0;
37 | }
38 | &-exit-done {
39 | opacity: 0;
40 | display: none;
41 | }
42 |
43 | .message {
44 | a {
45 | color: $linkColor;
46 | &:hover {
47 | text-decoration: underline;
48 | }
49 | }
50 | }
51 |
52 | .close {
53 | @media (pointer: fine) {
54 | visibility: hidden;
55 | }
56 | cursor: pointer;
57 | display: inline-block;
58 | font-size: 18px;
59 | height: 20px;
60 | width: 36px;
61 | line-height: 20px;
62 | padding: 0 0 0 6px;
63 | color: #ccc;
64 | &:hover {
65 | color: #fff;
66 | font-weight: bold;
67 | font-size: 20px;
68 | }
69 | }
70 | &:hover .close {
71 | @media (pointer: fine) {
72 | visibility: visible;
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/trending-filters/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import LanguageFilter from 'components/language-filter';
5 | import SpokenLanguageFilter from 'components/spoken-language-filter';
6 | import ViewFilter from 'components/view-filter';
7 | import styles from './styles.module.css';
8 | import TrendingPeriodFilter from './trending-period-filter';
9 |
10 | const TrendingFilters = (props) => (
11 |
12 |
13 |
17 |
18 |
19 |
23 |
24 |
25 |
29 |
30 |
31 |
35 |
36 |
37 | );
38 |
39 | TrendingFilters.propTypes = {
40 | updateSpokenLanguage: PropTypes.func.isRequired,
41 | updateLanguage: PropTypes.func.isRequired,
42 | updateViewType: PropTypes.func.isRequired,
43 | updateDateJump: PropTypes.func.isRequired,
44 | selectedSpokenLanguage: PropTypes.string,
45 | selectedLanguage: PropTypes.string,
46 | selectedViewType: PropTypes.string,
47 | selectedDateJump: PropTypes.string
48 | };
49 |
50 | export default TrendingFilters;
51 |
--------------------------------------------------------------------------------
/src/components/trending-filters/styles.module.css:
--------------------------------------------------------------------------------
1 | .filtersWrap {
2 | display: flex;
3 | }
4 | .filterItem:not(:first-child) {
5 | margin-left: 1em;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/trending-filters/trending-period-filter/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {trendingPeriodDefs} from 'lib/gh-trending';
4 | import SimpleSelect from 'components/simple-select';
5 | import {ReactComponent as Calendar} from 'icons/calendar.svg';
6 |
7 | const TrendingPeriodFilter = (props) => }
9 | onChange={props.updateDateJump}
10 | value={props.selectedDateJump}
11 | options={
12 | Object.keys(trendingPeriodDefs).map(k => ({value: k, label: trendingPeriodDefs[k].heading}))
13 | } />
14 |
15 | TrendingPeriodFilter.propTypes = {
16 | updateDateJump: PropTypes.func.isRequired,
17 | selectedDateJump: PropTypes.oneOf(Object.keys(trendingPeriodDefs)),
18 | };
19 |
20 | export default TrendingPeriodFilter;
21 |
--------------------------------------------------------------------------------
/src/components/view-filter/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | import PropTypes from 'prop-types';
4 |
5 | import {ReactComponent as Table} from 'icons/table.svg';
6 | import {ReactComponent as List} from 'icons/list.svg';
7 |
8 | import './styles.scss';
9 |
10 | class ViewFilter extends React.Component {
11 | state = {};
12 |
13 | changeSelected = (viewType) => {
14 | if (this.props.selectedViewType !== viewType) {
15 | this.props.updateViewType(viewType);
16 | }
17 | };
18 |
19 | render() {
20 | return (
21 |
22 |
23 |
this.changeSelected('grid') } className={ classNames({ active: this.props.selectedViewType === 'grid' }) }>
24 |
25 | Grid
26 |
27 |
this.changeSelected('list') } className={ classNames({ active: this.props.selectedViewType === 'list' }) }>
28 |
29 | List
30 |
31 |
32 |
33 | );
34 | }
35 | }
36 |
37 | ViewFilter.propTypes = {
38 | updateViewType: PropTypes.func.isRequired,
39 | selectedViewType: PropTypes.string,
40 | };
41 |
42 | export default ViewFilter;
43 |
--------------------------------------------------------------------------------
/src/components/view-filter/styles.scss:
--------------------------------------------------------------------------------
1 | @import 'theme.scss';
2 |
3 | .view-type {
4 | border-radius: 8px;
5 | padding: 10px;
6 | cursor: default;
7 |
8 | button {
9 | color: var(--text-faded-color);
10 | text-decoration: none;
11 | margin: 0 0 0 10px;
12 | padding: 0 5px;
13 | list-style: none;
14 | border: none;
15 | background: none;
16 | cursor: pointer;
17 | -webkit-appearance: none;
18 | box-shadow: none;
19 | outline: none;
20 | &.active, &:hover {
21 | color: var(--text-main-color);
22 | }
23 | &:first-child {
24 | margin-left: 0;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/containers/comments/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Disqus from 'disqus-react';
3 |
4 | // import './styles.css';
5 |
6 | export default class CommentsContainer extends React.Component {
7 | render() {
8 | return (
9 |
10 |
15 |
16 | );
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/containers/feed/feed.test.js:
--------------------------------------------------------------------------------
1 | // import React from 'react';
2 | // import ReactDOM from 'react-dom';
3 | import * as feed from './index.js';
4 |
5 | test('pathJoin', () => {
6 | expect(feed.pathJoin('/', '/a')).toBe('/a');
7 | expect(feed.pathJoin('a', 'b')).toBe('a/b');
8 | expect(feed.pathJoin('/a', 'b')).toBe('/a/b');
9 | expect(feed.pathJoin('a', 'b', 'c')).toBe('a/b/c');
10 | expect(feed.pathJoin('/a', 'b/', 'c/')).toBe('/a/b/c/');
11 | })
12 |
--------------------------------------------------------------------------------
/src/containers/feed/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, Route, Switch } from 'react-router-dom';
3 | import GitHubTrending from 'components/github-trending';
4 | import GitHubRanking from 'components/github-ranking';
5 | import ReactTooltip from 'react-tooltip'
6 |
7 | import styles from './styles.module.scss';
8 |
9 | /**
10 | * @example
11 | * pathJoin('/', 'a/','/c/') => "/a/c/"
12 | */
13 | export function pathJoin(...parts) {
14 | return parts.join('/').replace(/[/]+/g, '/')
15 | }
16 |
17 | function FeedContainer(props) {
18 | const baseURL = props.match.url;
19 |
20 | return (
21 |
22 |
23 |
26 |
27 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | )
41 | }
42 |
43 | export default FeedContainer;
44 |
--------------------------------------------------------------------------------
/src/containers/feed/styles.module.scss:
--------------------------------------------------------------------------------
1 | .listSwitcher {
2 | margin-bottom: 0.5rem;
3 | display: flex;
4 | align-items: center;
5 | opacity: 0.03;
6 |
7 | .listItem {
8 | cursor: pointer;
9 |
10 | $size: 0.85rem;
11 | margin: 0.2rem;
12 |
13 | width: $size;
14 | height: $size;
15 | border-radius: 50%;
16 | color: var(--dot-color);
17 | background-color: var(--dot-color);
18 |
19 | &:hover {
20 | box-shadow: 0 0 3px currentColor;
21 | }
22 |
23 | &:first-child {
24 | margin-left: 0;
25 | }
26 |
27 | // Dot colors
28 | &.A {
29 | --dot-color: orangered
30 | }
31 | &.B {
32 | --dot-color: darkviolet
33 | }
34 | &.C {
35 | --dot-color: limegreen
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/containers/options/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import './styles.css';
5 | import OptionsForm from '../../components/options-form';
6 | import { updateOptions } from '../../redux/preference/actions';
7 |
8 | class OptionsContainer extends React.Component {
9 | render() {
10 | return (
11 |
19 | );
20 | }
21 | }
22 |
23 | const mapStateToProps = (store) => {
24 | return {
25 | preference: store.preference
26 | };
27 | };
28 |
29 | const mapDispatchToProps = {
30 | updateOptions
31 | };
32 |
33 | export default connect(mapStateToProps, mapDispatchToProps)(OptionsContainer);
34 |
--------------------------------------------------------------------------------
/src/containers/options/styles.css:
--------------------------------------------------------------------------------
1 | .options-container {
2 | margin: 200px auto 20px;
3 | max-width: 700px;
4 | }
5 |
6 | .options-container .container {
7 | background: white;
8 | padding: 25px 30px;
9 | border-radius: 20px;
10 | }
11 |
--------------------------------------------------------------------------------
/src/custom-bootstrap.scss:
--------------------------------------------------------------------------------
1 |
2 | @import 'variables.scss';
3 |
4 | @import '~bootstrap/scss/functions';
5 | @import '~bootstrap/scss/variables';
6 | @import '~bootstrap/scss/mixins';
7 |
8 | @import "~bootstrap/scss/root";
9 | @import "~bootstrap/scss/reboot";
10 | @import "~bootstrap/scss/type";
11 | // @import "~bootstrap/scss/images";
12 | @import "~bootstrap/scss/code";
13 | @import "~bootstrap/scss/grid";
14 | // @import "~bootstrap/scss/tables";
15 | // @import "~bootstrap/scss/forms";
16 | // @import "~bootstrap/scss/buttons";
17 | @import "~bootstrap/scss/transitions";
18 | @import "~bootstrap/scss/dropdown";
19 | // @import "~bootstrap/scss/button-group";
20 | // @import "~bootstrap/scss/input-group";
21 | // @import "~bootstrap/scss/custom-forms";
22 | // @import "~bootstrap/scss/nav";
23 | // @import "~bootstrap/scss/navbar";
24 | // @import "~bootstrap/scss/card";
25 | // @import "~bootstrap/scss/breadcrumb";
26 | // @import "~bootstrap/scss/pagination";
27 | // @import "~bootstrap/scss/badge";
28 | // @import "~bootstrap/scss/jumbotron";
29 | @import "~bootstrap/scss/alert";
30 | // @import "~bootstrap/scss/progress";
31 | // @import "~bootstrap/scss/media";
32 | // @import "~bootstrap/scss/list-group";
33 | // @import "~bootstrap/scss/close";
34 | // @import "~bootstrap/scss/toasts";
35 | @import "~bootstrap/scss/modal";
36 | // @import "~bootstrap/scss/tooltip";
37 | // @import "~bootstrap/scss/popover";
38 | // @import "~bootstrap/scss/carousel";
39 | // @import "~bootstrap/scss/spinners";
40 |
41 | // @import "~bootstrap/scss/utilities";
42 | @import "~bootstrap/scss/utilities/align";
43 | @import "~bootstrap/scss/utilities/background";
44 | @import "~bootstrap/scss/utilities/borders";
45 | @import "~bootstrap/scss/utilities/clearfix";
46 | @import "~bootstrap/scss/utilities/display";
47 | @import "~bootstrap/scss/utilities/embed";
48 | @import "~bootstrap/scss/utilities/flex";
49 | @import "~bootstrap/scss/utilities/float";
50 | @import "~bootstrap/scss/utilities/overflow";
51 | @import "~bootstrap/scss/utilities/position";
52 | @import "~bootstrap/scss/utilities/shadows";
53 | @import "~bootstrap/scss/utilities/sizing";
54 | @import "~bootstrap/scss/utilities/stretched-link";
55 | @import "~bootstrap/scss/utilities/spacing";
56 | // @import "~bootstrap/scss/utilities/text";
57 | @import "~bootstrap/scss/utilities/visibility";
58 |
59 | // @import "~bootstrap/scss/print";
60 |
--------------------------------------------------------------------------------
/src/global.scss:
--------------------------------------------------------------------------------
1 | @import 'theme.scss';
2 |
3 | body {
4 | background: var(--background-primary-color);
5 | }
6 |
7 | // We apply themes from here!
8 | #page-wrap {
9 | min-height: 100vh;
10 | background: var(--background-primary-color);
11 | color: var(--text-head-color);
12 | }
13 |
14 | .text-muted {
15 | color: var(--text-secondary-color) !important;
16 | }
17 |
18 | // default button style
19 | button,
20 | .btn {
21 | font-size: 14px;
22 | padding: 10px 20px;
23 | border-radius: 8px;
24 | outline: 0;
25 | background-color: var(--primary-button-background-color);
26 | border: 1px solid var(--primary-button-background-color);
27 | color: var(--text-main-color);
28 |
29 | display: inline-flex;
30 | align-items: center;
31 |
32 | svg {
33 | margin-top: -2px;
34 | }
35 | // border: 1px solid #ccc;
36 | &:focus, &:active {
37 | outline: 0;
38 | }
39 | }
40 |
41 | .dropdown-menu {
42 | border: 1px solid var(--border-primary-color);
43 | box-shadow: 0 3px 12px var(--box-shadow-color);
44 | line-height: 2em;
45 | top: 3px !important;
46 | button {
47 | outline: none;
48 | }
49 | }
50 |
51 | .cursor-default {
52 | cursor: default !important;
53 | }
54 |
55 | .shadowed {
56 | box-shadow: 0 9px 62px -13px var(--box-shadow-color) !important;
57 | }
58 |
59 | @keyframes spin {
60 | 0% {
61 | -webkit-transform: rotate(0deg);
62 | transform: rotate(0deg);
63 | }
64 | 100% {
65 | -webkit-transform: rotate(359deg);
66 | transform: rotate(359deg);
67 | }
68 | }
69 | .spin {
70 | animation: spin 2s infinite linear;
71 | }
72 |
--------------------------------------------------------------------------------
/src/icons/bars-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/calendar.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/chrome-brands.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/code.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/comments-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/filter-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/fork.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Fork = (props) => (
4 |
5 |
7 |
8 | );
9 |
10 | export default Fork;
11 |
--------------------------------------------------------------------------------
/src/icons/github-brands.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/heart-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/list.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/period.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/spoken-language.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/star.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Star = (props) => (
4 |
5 |
6 |
7 | );
8 |
9 | // https://www.iconfont.cn/search/index?searchType=icon&q=star
10 | export const HalfStar = (props) => (
11 |
12 |
13 |
14 | );
15 |
16 | export const Stars = (props) => (
17 |
18 |
19 |
20 | );
21 |
22 | export default Star;
23 |
--------------------------------------------------------------------------------
/src/icons/sun.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default (props) => (
4 |
5 |
6 |
7 |
8 | {/* */}
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/src/icons/table.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/toggle-theme.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const ToggleTheme = (props) => (
4 |
5 | );
6 |
7 | export default ToggleTheme;
8 |
--------------------------------------------------------------------------------
/src/icons/twitter-brands.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/watcher.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Watcher = (props) => (
4 |
5 |
6 |
7 | );
8 |
9 | export default Watcher;
10 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import 'custom-bootstrap.scss';
2 | import 'global.scss';
3 |
4 | import ReactGA from 'react-ga';
5 | import React from 'react';
6 | import ReactDOM from 'react-dom';
7 | import App from './app';
8 |
9 | if (process.env.NODE_ENV === 'production') {
10 | ReactGA.initialize('UA-134825122-1', { standardImplementation: true });
11 | } else {
12 | ReactGA.initialize('foo', { testMode: true });
13 | }
14 |
15 | ReactDOM.render( , document.getElementById('root'));
16 |
--------------------------------------------------------------------------------
/src/lib/date-period.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A date period object is like below:
3 | * { start: , end: }
4 | * Or:
5 | * { spec: "Last 7 Days"}
6 | */
7 |
8 |
9 | export function humanizePeriod(period) {
10 | throw new Error('Not Implemented')
11 | }
12 |
13 | /**
14 | * @example "Last 7 Days" => {start: "2018-01-01", end: "2018-01-08"}
15 | */
16 | export function parsePeriodSpec(spec) {
17 | const lastNPat = /^Last\s(\d+)\s(Day|Week|Month|Year)s$/i;
18 | const pastPat = /^Past\s(Day|Week|Month|Year)$/i;
19 |
20 | let unit, N;
21 |
22 | let mat = lastNPat.exec(spec)
23 | if (mat) {
24 | N = parseInt(mat[1])
25 | unit = mat[2].toLocaleLowerCase()
26 | } else if ((mat = pastPat.exec(spec), mat)) {
27 | N = 1
28 | unit = mat[1].toLocaleLowerCase()
29 | } else if (spec.toLocaleLowerCase() === 'total history') {
30 | return {start: new Date('2007-10-29'), end: new Date()}
31 | } else {
32 | throw new Error(`Unknown date-period spec: ${spec}`)
33 | }
34 |
35 | const today = new Date()
36 | const preDate = new Date(today)
37 | switch (unit) {
38 | case 'day':
39 | preDate.setDate(preDate.getDate() - N)
40 | break;
41 | case 'week':
42 | preDate.setDate(preDate.getDate() - N*7)
43 | break;
44 | case 'month':
45 | preDate.setMonth(preDate.getMonth() - N)
46 | break;
47 | case 'year':
48 | preDate.setFullYear(preDate.getFullYear() - N);
49 | break;
50 | default:
51 | break;
52 | }
53 |
54 | return {start: preDate, end: today}
55 | }
56 |
57 | export function realizePeriod({spec, start, end}) {
58 | if (!spec && start && end) {
59 | return {start, end}
60 | }
61 |
62 | const parsed = parsePeriodSpec(spec)
63 | return {
64 | spec: spec,
65 | start: parsed.start,
66 | end: parsed.end,
67 | }
68 | }
69 |
70 | export function getYesterday() {
71 | var d = new Date();
72 | d.setDate(d.getDate() - 1);
73 | return d
74 | }
75 |
76 | export function getLastNDaysPeriod(N=7) {
77 | var today = new Date()
78 | var preDay = new Date()
79 | preDay.setDate(preDay.getDate() - N)
80 | return {
81 | start: preDay, end: today
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/lib/functools.js:
--------------------------------------------------------------------------------
1 | /**
2 | * pre-call a function and cache the result for only next use
3 | * @param fn: the function to be wrapped
4 | *
5 | * @example:
6 | * loadPrefernce = prefetch(loadPrefernce, 'HitUP:preference:GitHubTrending')
7 | * loadPrefernce('HitUP:preference:GitHubTrending') // should use cache
8 | * loadPrefernce('HitUP:preference:GitHubTrending') // should not use cache
9 | */
10 | export function prefetch(fn, ...args) {
11 | let cache = {};
12 |
13 | // do pre-call
14 | let k = JSON.stringify(args);
15 | let result = fn(...args);
16 | cache[k] = result;
17 |
18 | const popfunc = (...args) => {
19 | let k = JSON.stringify(args);
20 | if (k in cache) {
21 | let result = cache[k];
22 | // because it's only a one-time cache
23 | delete cache[k];
24 | return result;
25 | } else {
26 | let result = fn(...args);
27 | return result;
28 | }
29 | }
30 |
31 | return popfunc
32 | }
33 |
--------------------------------------------------------------------------------
/src/lib/functools.test.js:
--------------------------------------------------------------------------------
1 | import { prefetch } from './functools';
2 |
3 | test('functools.prefetch', async () => {
4 | let testObj = {a: 1, b: 2, c: 3}
5 | let sumCallTimes = 0;
6 | let result;
7 |
8 | function sum(...ks) {
9 | sumCallTimes++;
10 | return ks.map(k => testObj[k]).reduce((x, y) => x + y, 0);
11 | }
12 |
13 | expect(sum()).toBe(0);
14 | expect(sum('a')).toBe(1);
15 | expect(sum('a', 'c')).toBe(4);
16 | expect(sum('a', 'b', 'c')).toBe(6);
17 |
18 | expect(sumCallTimes).toBe(4);
19 |
20 | const prefetched = prefetch(sum, 'b', 'c');
21 | expect(sumCallTimes).toBe(5);
22 |
23 | // fetching without cache is not affected
24 | result = prefetched('a', 'b')
25 | expect(sumCallTimes).toBe(6);
26 | expect(result).toBe(3);
27 |
28 | // test prefetched OK
29 | testObj.c = 3.1;
30 | result = prefetched('b', 'c');
31 | expect(sumCallTimes).toBe(6);
32 | expect(result).toBe(5);
33 |
34 | // test cache is used only one time
35 | result = prefetched('b', 'c');
36 | expect(sumCallTimes).toBe(7);
37 | expect(result).toBe(5.1);
38 | })
39 |
--------------------------------------------------------------------------------
/src/lib/gh-repo-search/index.js:
--------------------------------------------------------------------------------
1 | import {baseQuery} from './query-tmpl';
2 | import TinyCache from 'tinycache';
3 | import {realizePeriod} from 'lib/date-period';
4 |
5 | let cache = new TinyCache();
6 |
7 | export default async function repoSearch({
8 | accessToken,
9 | qwords='',
10 | fork=true,
11 | first=30, // i.e. page-size
12 | after=null,
13 | createdPeriod=null,
14 | searchIn=['name', 'description'],
15 | language=null,
16 | topic=null,
17 | license=null,
18 | sort='stars-desc',
19 | }) {
20 | if (!accessToken) {
21 | throw new Error('Access token is required to access GitHub GraphQL API v4.')
22 | }
23 |
24 | console.log(realizePeriod(createdPeriod));
25 |
26 | function buildQstr() {
27 | const qparts = [];
28 | if (qwords) {
29 | qparts.push(qwords)
30 | }
31 | // qparts.push(`is:public`)
32 | if (fork) {
33 | qparts.push(`fork:true`)
34 | }
35 | if (sort && sort.startsWith('stars')) {
36 | qparts.push(`stars:>0`)
37 | // NOTE: without `stars:>0` the first item of seach result canbe random!
38 | }
39 | if (topic) {
40 | qparts.push(`topic:${topic}`)
41 | }
42 | if (language) {
43 | qparts.push(`language:${language}`)
44 | }
45 | if (sort) {
46 | qparts.push(`sort:${sort}`)
47 | }
48 | if (qwords && searchIn && searchIn.length) {
49 | qparts.push(`in:${searchIn.join(',')}`)
50 | }
51 |
52 | return qparts.join(' ')
53 | }
54 |
55 | const gqlVars = {
56 | "qstr": buildQstr(),
57 | "first": first,
58 | "after": after || null,
59 | }
60 | const cacheKey = `qstr:${gqlVars.qstr};first:${gqlVars.first};after:${gqlVars.after}`
61 | const reqBody = JSON.stringify({
62 | query: baseQuery,
63 | variables: gqlVars,
64 | })
65 |
66 | console.debug(
67 | `Searching GitHub repos with query: ` +
68 | `${baseQuery}\nvariables: ${JSON.stringify(gqlVars)}` +
69 | `\nCache key: "${cacheKey}"`
70 | );
71 |
72 | let data = cache.get(cacheKey);
73 | if (data) {
74 | return data
75 | }
76 |
77 | const resp = await fetch('https://api.github.com/graphql', {
78 | method: 'POST',
79 | headers: {
80 | 'Content-Type': 'application/json',
81 | 'Authorization': `Bearer ${accessToken}`,
82 | },
83 | body: reqBody,
84 | })
85 | data = await resp.json()
86 | cache.put(cacheKey, data, 1000 * 300)
87 |
88 | return data
89 | }
90 |
--------------------------------------------------------------------------------
/src/lib/gh-repo-search/query-tmpl.js:
--------------------------------------------------------------------------------
1 | export const baseQuery = `query searchRepos($qstr: String!, $first: Int!, $after: String) {
2 | search(
3 | query: $qstr,
4 | type: REPOSITORY,
5 | first: $first,
6 | after: $after
7 | ) {
8 | repositoryCount
9 | pageInfo {
10 | startCursor
11 | endCursor
12 | }
13 | nodes {
14 | ... on Repository {
15 | name
16 | url
17 | createdAt
18 | updatedAt
19 | pushedAt
20 | description
21 | primaryLanguage {color name}
22 | isFork
23 | forkCount
24 | repositoryTopics(first:5) {
25 | nodes {
26 | #resourcePath
27 | topic {
28 | name
29 | #relatedTopics {
30 | # name
31 | #}
32 | }
33 | }
34 | }
35 | stargazers {
36 | totalCount
37 | }
38 | owner {
39 | login
40 | avatarUrl
41 | }
42 | }
43 | }
44 | }
45 | }`
46 |
--------------------------------------------------------------------------------
/src/lib/gh-trending/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import lscache from 'lscache';
3 | import ReactGA from 'react-ga';
4 |
5 | const TRENDING_API_URL = 'https://github-trending-api-wonder.herokuapp.com/repositories';
6 |
7 | export const trendingPeriodDefs = {
8 | 'day': {heading: 'Today', ghParamKey: 'daily'},
9 | 'week': {heading: 'This Week', ghParamKey: 'weekly'},
10 | 'month': {heading: 'This Month', ghParamKey: 'monthly'},
11 | };
12 |
13 | const adaptFilters = (filters) => {
14 | const transformedFilters = {};
15 |
16 | transformedFilters.spoken_language_code = filters.spokenLanguage
17 | transformedFilters.language = filters.language
18 | transformedFilters.since = {
19 | 'week': 'weekly',
20 | 'month': 'monthly',
21 | // 'year': '',
22 | 'day': 'daily'
23 | }[filters.dateJump]
24 |
25 | return transformedFilters;
26 | };
27 |
28 | export async function fetchTrendingRepositories(filters) {
29 | const {spoken_language_code, language, since} = adaptFilters(filters);
30 |
31 | let dataLabel = `${spoken_language_code || "all"}/${language || "all"}/${since}`;
32 | let cacheKey = `github-trending-repositories:${dataLabel}`;
33 | let reposities;
34 |
35 | reposities = lscache.get(cacheKey);
36 | if (reposities) {
37 | console.debug(`Got trending repositories from cache. (cacheKey: ${cacheKey})`);
38 | return reposities;
39 | }
40 |
41 | console.debug(`Fetching trending repositories (${dataLabel}).`);
42 | let resp = await axios.get(TRENDING_API_URL, {
43 | params: {language, since, spoken_language_code}
44 | }).catch(error => {
45 | ReactGA.exception({
46 | description: `Failed to Fetch Trending Data: ${error.message}. detail: ${JSON.stringify(error)}`,
47 | fatal: true
48 | });
49 | throw(error);
50 | });
51 |
52 | ReactGA.event({
53 | category: 'API',
54 | label: 'Trending API Call',
55 | action: `Done Fetch Trending Data (${dataLabel})`
56 | });
57 |
58 | reposities = resp.data;
59 |
60 | reposities.forEach((repo) => {
61 | let ownerLogin = repo.author;
62 | repo.html_url = repo.url;
63 | repo.owner = {
64 | login: ownerLogin,
65 | html_url: `https://github.com/${ownerLogin}`,
66 | avatar_url: `https://avatars.githubusercontent.com/${ownerLogin}?s=200&v=4`,
67 | }
68 | });
69 |
70 | // console.debug(`Got trending data (${dataLabel}):`, reposities);
71 |
72 | if (reposities && reposities.length > 0) {
73 | lscache.set(cacheKey, reposities, 60);
74 | }
75 | return reposities;
76 | }
77 |
--------------------------------------------------------------------------------
/src/lib/github/index.js:
--------------------------------------------------------------------------------
1 | export {default as languages} from './languages.json';
2 | export {default as spokenLanguages} from './spoken-languages.json';
3 |
--------------------------------------------------------------------------------
/src/lib/github/languages.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "All Languages",
4 | "value": ""
5 | },
6 | {
7 | "name": "Python",
8 | "value": "python"
9 | },
10 | {
11 | "name": "C",
12 | "value": "c"
13 | },
14 | {
15 | "name": "C++",
16 | "value": "c%2B%2B"
17 | },
18 | {
19 | "name": "Java",
20 | "value": "java"
21 | },
22 | {
23 | "name": "JavaScript",
24 | "value": "javascript"
25 | },
26 | {
27 | "name": "Ruby",
28 | "value": "ruby"
29 | },
30 | {
31 | "name": "PHP",
32 | "value": "php"
33 | },
34 | {
35 | "name": "HTML",
36 | "value": "html"
37 | },
38 | {
39 | "name": "Rust",
40 | "value": "rust"
41 | },
42 | {
43 | "name": "Go",
44 | "value": "go"
45 | },
46 | {
47 | "name": "Shell",
48 | "value": "shell"
49 | },
50 | {
51 | "name": "1C Enterprise",
52 | "value": "1c-enterprise"
53 | },
54 | {
55 | "name": "ABAP",
56 | "value": "abap"
57 | },
58 | {
59 | "name": "ABNF",
60 | "value": "abnf"
61 | },
62 | {
63 | "name": "ActionScript",
64 | "value": "actionscript"
65 | },
66 | {
67 | "name": "Ada",
68 | "value": "ada"
69 | },
70 | {
71 | "name": "Adobe Font Metrics",
72 | "value": "adobe-font-metrics"
73 | },
74 | {
75 | "name": "Agda",
76 | "value": "agda"
77 | },
78 | {
79 | "name": "AGS Script",
80 | "value": "ags-script"
81 | },
82 | {
83 | "name": "Alloy",
84 | "value": "alloy"
85 | },
86 | {
87 | "name": "Alpine Abuild",
88 | "value": "alpine-abuild"
89 | },
90 | {
91 | "name": "AMPL",
92 | "value": "ampl"
93 | },
94 | {
95 | "name": "AngelScript",
96 | "value": "angelscript"
97 | },
98 | {
99 | "name": "Ant Build System",
100 | "value": "ant-build-system"
101 | },
102 | {
103 | "name": "ANTLR",
104 | "value": "antlr"
105 | },
106 | {
107 | "name": "ApacheConf",
108 | "value": "apacheconf"
109 | },
110 | {
111 | "name": "Apex",
112 | "value": "apex"
113 | },
114 | {
115 | "name": "API Blueprint",
116 | "value": "api-blueprint"
117 | },
118 | {
119 | "name": "APL",
120 | "value": "apl"
121 | },
122 | {
123 | "name": "Apollo Guidance Computer",
124 | "value": "apollo-guidance-computer"
125 | },
126 | {
127 | "name": "AppleScript",
128 | "value": "applescript"
129 | },
130 | {
131 | "name": "Arc",
132 | "value": "arc"
133 | },
134 | {
135 | "name": "AsciiDoc",
136 | "value": "asciidoc"
137 | },
138 | {
139 | "name": "ASN.1",
140 | "value": "asn.1"
141 | },
142 | {
143 | "name": "ASP",
144 | "value": "asp"
145 | },
146 | {
147 | "name": "AspectJ",
148 | "value": "aspectj"
149 | },
150 | {
151 | "name": "Assembly",
152 | "value": "assembly"
153 | },
154 | {
155 | "name": "ATS",
156 | "value": "ats"
157 | },
158 | {
159 | "name": "Augeas",
160 | "value": "augeas"
161 | },
162 | {
163 | "name": "AutoHotkey",
164 | "value": "autohotkey"
165 | },
166 | {
167 | "name": "AutoIt",
168 | "value": "autoit"
169 | },
170 | {
171 | "name": "Awk",
172 | "value": "awk"
173 | },
174 | {
175 | "name": "Ballerina",
176 | "value": "ballerina"
177 | },
178 | {
179 | "name": "Batchfile",
180 | "value": "batchfile"
181 | },
182 | {
183 | "name": "Befunge",
184 | "value": "befunge"
185 | },
186 | {
187 | "name": "Bison",
188 | "value": "bison"
189 | },
190 | {
191 | "name": "BitBake",
192 | "value": "bitbake"
193 | },
194 | {
195 | "name": "Blade",
196 | "value": "blade"
197 | },
198 | {
199 | "name": "BlitzBasic",
200 | "value": "blitzbasic"
201 | },
202 | {
203 | "name": "BlitzMax",
204 | "value": "blitzmax"
205 | },
206 | {
207 | "name": "Bluespec",
208 | "value": "bluespec"
209 | },
210 | {
211 | "name": "Boo",
212 | "value": "boo"
213 | },
214 | {
215 | "name": "Brainfuck",
216 | "value": "brainfuck"
217 | },
218 | {
219 | "name": "Brightscript",
220 | "value": "brightscript"
221 | },
222 | {
223 | "name": "Bro",
224 | "value": "bro"
225 | },
226 | {
227 | "name": "C#",
228 | "value": "c%23"
229 | },
230 | {
231 | "name": "C-ObjDump",
232 | "value": "c-objdump"
233 | },
234 | {
235 | "name": "C2hs Haskell",
236 | "value": "c2hs-haskell"
237 | },
238 | {
239 | "name": "Cap'n Proto",
240 | "value": "cap'n-proto"
241 | },
242 | {
243 | "name": "CartoCSS",
244 | "value": "cartocss"
245 | },
246 | {
247 | "name": "Ceylon",
248 | "value": "ceylon"
249 | },
250 | {
251 | "name": "Chapel",
252 | "value": "chapel"
253 | },
254 | {
255 | "name": "Charity",
256 | "value": "charity"
257 | },
258 | {
259 | "name": "ChucK",
260 | "value": "chuck"
261 | },
262 | {
263 | "name": "Cirru",
264 | "value": "cirru"
265 | },
266 | {
267 | "name": "Clarion",
268 | "value": "clarion"
269 | },
270 | {
271 | "name": "Clean",
272 | "value": "clean"
273 | },
274 | {
275 | "name": "Click",
276 | "value": "click"
277 | },
278 | {
279 | "name": "CLIPS",
280 | "value": "clips"
281 | },
282 | {
283 | "name": "Clojure",
284 | "value": "clojure"
285 | },
286 | {
287 | "name": "Closure Templates",
288 | "value": "closure-templates"
289 | },
290 | {
291 | "name": "CMake",
292 | "value": "cmake"
293 | },
294 | {
295 | "name": "COBOL",
296 | "value": "cobol"
297 | },
298 | {
299 | "name": "CoffeeScript",
300 | "value": "coffeescript"
301 | },
302 | {
303 | "name": "ColdFusion",
304 | "value": "coldfusion"
305 | },
306 | {
307 | "name": "ColdFusion CFC",
308 | "value": "coldfusion-cfc"
309 | },
310 | {
311 | "name": "COLLADA",
312 | "value": "collada"
313 | },
314 | {
315 | "name": "Common Lisp",
316 | "value": "common-lisp"
317 | },
318 | {
319 | "name": "Common Workflow Language",
320 | "value": "common-workflow-language"
321 | },
322 | {
323 | "name": "Component Pascal",
324 | "value": "component-pascal"
325 | },
326 | {
327 | "name": "Cool",
328 | "value": "cool"
329 | },
330 | {
331 | "name": "Coq",
332 | "value": "coq"
333 | },
334 | {
335 | "name": "Cpp-ObjDump",
336 | "value": "cpp-objdump"
337 | },
338 | {
339 | "name": "Creole",
340 | "value": "creole"
341 | },
342 | {
343 | "name": "Crystal",
344 | "value": "crystal"
345 | },
346 | {
347 | "name": "CSON",
348 | "value": "cson"
349 | },
350 | {
351 | "name": "Csound",
352 | "value": "csound"
353 | },
354 | {
355 | "name": "Csound Document",
356 | "value": "csound-document"
357 | },
358 | {
359 | "name": "Csound Score",
360 | "value": "csound-score"
361 | },
362 | {
363 | "name": "CSS",
364 | "value": "css"
365 | },
366 | {
367 | "name": "CSV",
368 | "value": "csv"
369 | },
370 | {
371 | "name": "Cuda",
372 | "value": "cuda"
373 | },
374 | {
375 | "name": "CWeb",
376 | "value": "cweb"
377 | },
378 | {
379 | "name": "Cycript",
380 | "value": "cycript"
381 | },
382 | {
383 | "name": "Cython",
384 | "value": "cython"
385 | },
386 | {
387 | "name": "D",
388 | "value": "d"
389 | },
390 | {
391 | "name": "D-ObjDump",
392 | "value": "d-objdump"
393 | },
394 | {
395 | "name": "Darcs Patch",
396 | "value": "darcs-patch"
397 | },
398 | {
399 | "name": "Dart",
400 | "value": "dart"
401 | },
402 | {
403 | "name": "DataWeave",
404 | "value": "dataweave"
405 | },
406 | {
407 | "name": "desktop",
408 | "value": "desktop"
409 | },
410 | {
411 | "name": "Diff",
412 | "value": "diff"
413 | },
414 | {
415 | "name": "DIGITAL Command Language",
416 | "value": "digital-command-language"
417 | },
418 | {
419 | "name": "DM",
420 | "value": "dm"
421 | },
422 | {
423 | "name": "DNS Zone",
424 | "value": "dns-zone"
425 | },
426 | {
427 | "name": "Dockerfile",
428 | "value": "dockerfile"
429 | },
430 | {
431 | "name": "Dogescript",
432 | "value": "dogescript"
433 | },
434 | {
435 | "name": "DTrace",
436 | "value": "dtrace"
437 | },
438 | {
439 | "name": "Dylan",
440 | "value": "dylan"
441 | },
442 | {
443 | "name": "E",
444 | "value": "e"
445 | },
446 | {
447 | "name": "Eagle",
448 | "value": "eagle"
449 | },
450 | {
451 | "name": "Easybuild",
452 | "value": "easybuild"
453 | },
454 | {
455 | "name": "EBNF",
456 | "value": "ebnf"
457 | },
458 | {
459 | "name": "eC",
460 | "value": "ec"
461 | },
462 | {
463 | "name": "Ecere Projects",
464 | "value": "ecere-projects"
465 | },
466 | {
467 | "name": "ECL",
468 | "value": "ecl"
469 | },
470 | {
471 | "name": "ECLiPSe",
472 | "value": "eclipse"
473 | },
474 | {
475 | "name": "Edje Data Collection",
476 | "value": "edje-data-collection"
477 | },
478 | {
479 | "name": "edn",
480 | "value": "edn"
481 | },
482 | {
483 | "name": "Eiffel",
484 | "value": "eiffel"
485 | },
486 | {
487 | "name": "EJS",
488 | "value": "ejs"
489 | },
490 | {
491 | "name": "Elixir",
492 | "value": "elixir"
493 | },
494 | {
495 | "name": "Elm",
496 | "value": "elm"
497 | },
498 | {
499 | "name": "Emacs Lisp",
500 | "value": "emacs-lisp"
501 | },
502 | {
503 | "name": "EmberScript",
504 | "value": "emberscript"
505 | },
506 | {
507 | "name": "EQ",
508 | "value": "eq"
509 | },
510 | {
511 | "name": "Erlang",
512 | "value": "erlang"
513 | },
514 | {
515 | "name": "F#",
516 | "value": "f%23"
517 | },
518 | {
519 | "name": "Factor",
520 | "value": "factor"
521 | },
522 | {
523 | "name": "Fancy",
524 | "value": "fancy"
525 | },
526 | {
527 | "name": "Fantom",
528 | "value": "fantom"
529 | },
530 | {
531 | "name": "Filebench WML",
532 | "value": "filebench-wml"
533 | },
534 | {
535 | "name": "Filterscript",
536 | "value": "filterscript"
537 | },
538 | {
539 | "name": "fish",
540 | "value": "fish"
541 | },
542 | {
543 | "name": "FLUX",
544 | "value": "flux"
545 | },
546 | {
547 | "name": "Formatted",
548 | "value": "formatted"
549 | },
550 | {
551 | "name": "Forth",
552 | "value": "forth"
553 | },
554 | {
555 | "name": "Fortran",
556 | "value": "fortran"
557 | },
558 | {
559 | "name": "FreeMarker",
560 | "value": "freemarker"
561 | },
562 | {
563 | "name": "Frege",
564 | "value": "frege"
565 | },
566 | {
567 | "name": "G-code",
568 | "value": "g-code"
569 | },
570 | {
571 | "name": "Game Maker Language",
572 | "value": "game-maker-language"
573 | },
574 | {
575 | "name": "GAMS",
576 | "value": "gams"
577 | },
578 | {
579 | "name": "GAP",
580 | "value": "gap"
581 | },
582 | {
583 | "name": "GCC Machine Description",
584 | "value": "gcc-machine-description"
585 | },
586 | {
587 | "name": "GDB",
588 | "value": "gdb"
589 | },
590 | {
591 | "name": "GDScript",
592 | "value": "gdscript"
593 | },
594 | {
595 | "name": "Genie",
596 | "value": "genie"
597 | },
598 | {
599 | "name": "Genshi",
600 | "value": "genshi"
601 | },
602 | {
603 | "name": "Gentoo Ebuild",
604 | "value": "gentoo-ebuild"
605 | },
606 | {
607 | "name": "Gentoo Eclass",
608 | "value": "gentoo-eclass"
609 | },
610 | {
611 | "name": "Gerber Image",
612 | "value": "gerber-image"
613 | },
614 | {
615 | "name": "Gettext Catalog",
616 | "value": "gettext-catalog"
617 | },
618 | {
619 | "name": "Gherkin",
620 | "value": "gherkin"
621 | },
622 | {
623 | "name": "GLSL",
624 | "value": "glsl"
625 | },
626 | {
627 | "name": "Glyph",
628 | "value": "glyph"
629 | },
630 | {
631 | "name": "GN",
632 | "value": "gn"
633 | },
634 | {
635 | "name": "Gnuplot",
636 | "value": "gnuplot"
637 | },
638 | {
639 | "name": "Golo",
640 | "value": "golo"
641 | },
642 | {
643 | "name": "Gosu",
644 | "value": "gosu"
645 | },
646 | {
647 | "name": "Grace",
648 | "value": "grace"
649 | },
650 | {
651 | "name": "Gradle",
652 | "value": "gradle"
653 | },
654 | {
655 | "name": "Grammatical Framework",
656 | "value": "grammatical-framework"
657 | },
658 | {
659 | "name": "Graph Modeling Language",
660 | "value": "graph-modeling-language"
661 | },
662 | {
663 | "name": "GraphQL",
664 | "value": "graphql"
665 | },
666 | {
667 | "name": "Graphviz (DOT)",
668 | "value": "graphviz-(dot)"
669 | },
670 | {
671 | "name": "Groovy",
672 | "value": "groovy"
673 | },
674 | {
675 | "name": "Groovy Server Pages",
676 | "value": "groovy-server-pages"
677 | },
678 | {
679 | "name": "Hack",
680 | "value": "hack"
681 | },
682 | {
683 | "name": "Haml",
684 | "value": "haml"
685 | },
686 | {
687 | "name": "Handlebars",
688 | "value": "handlebars"
689 | },
690 | {
691 | "name": "Harbour",
692 | "value": "harbour"
693 | },
694 | {
695 | "name": "Haskell",
696 | "value": "haskell"
697 | },
698 | {
699 | "name": "Haxe",
700 | "value": "haxe"
701 | },
702 | {
703 | "name": "HCL",
704 | "value": "hcl"
705 | },
706 | {
707 | "name": "HLSL",
708 | "value": "hlsl"
709 | },
710 | {
711 | "name": "HTML+Django",
712 | "value": "html%2Bdjango"
713 | },
714 | {
715 | "name": "HTML+ECR",
716 | "value": "html%2Becr"
717 | },
718 | {
719 | "name": "HTML+EEX",
720 | "value": "html%2Beex"
721 | },
722 | {
723 | "name": "HTML+ERB",
724 | "value": "html%2Berb"
725 | },
726 | {
727 | "name": "HTML+PHP",
728 | "value": "html%2Bphp"
729 | },
730 | {
731 | "name": "HTTP",
732 | "value": "http"
733 | },
734 | {
735 | "name": "Hy",
736 | "value": "hy"
737 | },
738 | {
739 | "name": "HyPhy",
740 | "value": "hyphy"
741 | },
742 | {
743 | "name": "IDL",
744 | "value": "idl"
745 | },
746 | {
747 | "name": "Idris",
748 | "value": "idris"
749 | },
750 | {
751 | "name": "IGOR Pro",
752 | "value": "igor-pro"
753 | },
754 | {
755 | "name": "Inform 7",
756 | "value": "inform-7"
757 | },
758 | {
759 | "name": "INI",
760 | "value": "ini"
761 | },
762 | {
763 | "name": "Inno Setup",
764 | "value": "inno-setup"
765 | },
766 | {
767 | "name": "Io",
768 | "value": "io"
769 | },
770 | {
771 | "name": "Ioke",
772 | "value": "ioke"
773 | },
774 | {
775 | "name": "IRC log",
776 | "value": "irc-log"
777 | },
778 | {
779 | "name": "Isabelle",
780 | "value": "isabelle"
781 | },
782 | {
783 | "name": "Isabelle ROOT",
784 | "value": "isabelle-root"
785 | },
786 | {
787 | "name": "J",
788 | "value": "j"
789 | },
790 | {
791 | "name": "Jasmin",
792 | "value": "jasmin"
793 | },
794 | {
795 | "name": "Java Server Pages",
796 | "value": "java-server-pages"
797 | },
798 | {
799 | "name": "JFlex",
800 | "value": "jflex"
801 | },
802 | {
803 | "name": "Jison",
804 | "value": "jison"
805 | },
806 | {
807 | "name": "Jison Lex",
808 | "value": "jison-lex"
809 | },
810 | {
811 | "name": "Jolie",
812 | "value": "jolie"
813 | },
814 | {
815 | "name": "JSON",
816 | "value": "json"
817 | },
818 | {
819 | "name": "JSON5",
820 | "value": "json5"
821 | },
822 | {
823 | "name": "JSONiq",
824 | "value": "jsoniq"
825 | },
826 | {
827 | "name": "JSONLD",
828 | "value": "jsonld"
829 | },
830 | {
831 | "name": "JSX",
832 | "value": "jsx"
833 | },
834 | {
835 | "name": "Julia",
836 | "value": "julia"
837 | },
838 | {
839 | "name": "Jupyter Notebook",
840 | "value": "jupyter-notebook"
841 | },
842 | {
843 | "name": "KiCad Layout",
844 | "value": "kicad-layout"
845 | },
846 | {
847 | "name": "KiCad Legacy Layout",
848 | "value": "kicad-legacy-layout"
849 | },
850 | {
851 | "name": "KiCad Schematic",
852 | "value": "kicad-schematic"
853 | },
854 | {
855 | "name": "Kit",
856 | "value": "kit"
857 | },
858 | {
859 | "name": "Kotlin",
860 | "value": "kotlin"
861 | },
862 | {
863 | "name": "KRL",
864 | "value": "krl"
865 | },
866 | {
867 | "name": "LabVIEW",
868 | "value": "labview"
869 | },
870 | {
871 | "name": "Lasso",
872 | "value": "lasso"
873 | },
874 | {
875 | "name": "Latte",
876 | "value": "latte"
877 | },
878 | {
879 | "name": "Lean",
880 | "value": "lean"
881 | },
882 | {
883 | "name": "Less",
884 | "value": "less"
885 | },
886 | {
887 | "name": "Lex",
888 | "value": "lex"
889 | },
890 | {
891 | "name": "LFE",
892 | "value": "lfe"
893 | },
894 | {
895 | "name": "LilyPond",
896 | "value": "lilypond"
897 | },
898 | {
899 | "name": "Limbo",
900 | "value": "limbo"
901 | },
902 | {
903 | "name": "Linker Script",
904 | "value": "linker-script"
905 | },
906 | {
907 | "name": "Linux Kernel Module",
908 | "value": "linux-kernel-module"
909 | },
910 | {
911 | "name": "Liquid",
912 | "value": "liquid"
913 | },
914 | {
915 | "name": "Literate Agda",
916 | "value": "literate-agda"
917 | },
918 | {
919 | "name": "Literate CoffeeScript",
920 | "value": "literate-coffeescript"
921 | },
922 | {
923 | "name": "Literate Haskell",
924 | "value": "literate-haskell"
925 | },
926 | {
927 | "name": "LiveScript",
928 | "value": "livescript"
929 | },
930 | {
931 | "name": "LLVM",
932 | "value": "llvm"
933 | },
934 | {
935 | "name": "Logos",
936 | "value": "logos"
937 | },
938 | {
939 | "name": "Logtalk",
940 | "value": "logtalk"
941 | },
942 | {
943 | "name": "LOLCODE",
944 | "value": "lolcode"
945 | },
946 | {
947 | "name": "LookML",
948 | "value": "lookml"
949 | },
950 | {
951 | "name": "LoomScript",
952 | "value": "loomscript"
953 | },
954 | {
955 | "name": "LSL",
956 | "value": "lsl"
957 | },
958 | {
959 | "name": "Lua",
960 | "value": "lua"
961 | },
962 | {
963 | "name": "M",
964 | "value": "m"
965 | },
966 | {
967 | "name": "M4",
968 | "value": "m4"
969 | },
970 | {
971 | "name": "M4Sugar",
972 | "value": "m4sugar"
973 | },
974 | {
975 | "name": "Makefile",
976 | "value": "makefile"
977 | },
978 | {
979 | "name": "Mako",
980 | "value": "mako"
981 | },
982 | {
983 | "name": "Markdown",
984 | "value": "markdown"
985 | },
986 | {
987 | "name": "Marko",
988 | "value": "marko"
989 | },
990 | {
991 | "name": "Mask",
992 | "value": "mask"
993 | },
994 | {
995 | "name": "Mathematica",
996 | "value": "mathematica"
997 | },
998 | {
999 | "name": "Matlab",
1000 | "value": "matlab"
1001 | },
1002 | {
1003 | "name": "Maven POM",
1004 | "value": "maven-pom"
1005 | },
1006 | {
1007 | "name": "Max",
1008 | "value": "max"
1009 | },
1010 | {
1011 | "name": "MAXScript",
1012 | "value": "maxscript"
1013 | },
1014 | {
1015 | "name": "MediaWiki",
1016 | "value": "mediawiki"
1017 | },
1018 | {
1019 | "name": "Mercury",
1020 | "value": "mercury"
1021 | },
1022 | {
1023 | "name": "Meson",
1024 | "value": "meson"
1025 | },
1026 | {
1027 | "name": "Metal",
1028 | "value": "metal"
1029 | },
1030 | {
1031 | "name": "MiniD",
1032 | "value": "minid"
1033 | },
1034 | {
1035 | "name": "Mirah",
1036 | "value": "mirah"
1037 | },
1038 | {
1039 | "name": "Modelica",
1040 | "value": "modelica"
1041 | },
1042 | {
1043 | "name": "Modula-2",
1044 | "value": "modula-2"
1045 | },
1046 | {
1047 | "name": "Module Management System",
1048 | "value": "module-management-system"
1049 | },
1050 | {
1051 | "name": "Monkey",
1052 | "value": "monkey"
1053 | },
1054 | {
1055 | "name": "Moocode",
1056 | "value": "moocode"
1057 | },
1058 | {
1059 | "name": "MoonScript",
1060 | "value": "moonscript"
1061 | },
1062 | {
1063 | "name": "MQL4",
1064 | "value": "mql4"
1065 | },
1066 | {
1067 | "name": "MQL5",
1068 | "value": "mql5"
1069 | },
1070 | {
1071 | "name": "MTML",
1072 | "value": "mtml"
1073 | },
1074 | {
1075 | "name": "MUF",
1076 | "value": "muf"
1077 | },
1078 | {
1079 | "name": "mupad",
1080 | "value": "mupad"
1081 | },
1082 | {
1083 | "name": "Myghty",
1084 | "value": "myghty"
1085 | },
1086 | {
1087 | "name": "NCL",
1088 | "value": "ncl"
1089 | },
1090 | {
1091 | "name": "Nearley",
1092 | "value": "nearley"
1093 | },
1094 | {
1095 | "name": "Nemerle",
1096 | "value": "nemerle"
1097 | },
1098 | {
1099 | "name": "nesC",
1100 | "value": "nesc"
1101 | },
1102 | {
1103 | "name": "NetLinx",
1104 | "value": "netlinx"
1105 | },
1106 | {
1107 | "name": "NetLinx+ERB",
1108 | "value": "netlinx%2Berb"
1109 | },
1110 | {
1111 | "name": "NetLogo",
1112 | "value": "netlogo"
1113 | },
1114 | {
1115 | "name": "NewLisp",
1116 | "value": "newlisp"
1117 | },
1118 | {
1119 | "name": "Nextflow",
1120 | "value": "nextflow"
1121 | },
1122 | {
1123 | "name": "Nginx",
1124 | "value": "nginx"
1125 | },
1126 | {
1127 | "name": "Nim",
1128 | "value": "nim"
1129 | },
1130 | {
1131 | "name": "Ninja",
1132 | "value": "ninja"
1133 | },
1134 | {
1135 | "name": "Nit",
1136 | "value": "nit"
1137 | },
1138 | {
1139 | "name": "Nix",
1140 | "value": "nix"
1141 | },
1142 | {
1143 | "name": "NL",
1144 | "value": "nl"
1145 | },
1146 | {
1147 | "name": "NSIS",
1148 | "value": "nsis"
1149 | },
1150 | {
1151 | "name": "Nu",
1152 | "value": "nu"
1153 | },
1154 | {
1155 | "name": "NumPy",
1156 | "value": "numpy"
1157 | },
1158 | {
1159 | "name": "ObjDump",
1160 | "value": "objdump"
1161 | },
1162 | {
1163 | "name": "Objective-C",
1164 | "value": "objective-c"
1165 | },
1166 | {
1167 | "name": "Objective-C++",
1168 | "value": "objective-c%2B%2B"
1169 | },
1170 | {
1171 | "name": "Objective-J",
1172 | "value": "objective-j"
1173 | },
1174 | {
1175 | "name": "OCaml",
1176 | "value": "ocaml"
1177 | },
1178 | {
1179 | "name": "Omgrofl",
1180 | "value": "omgrofl"
1181 | },
1182 | {
1183 | "name": "ooc",
1184 | "value": "ooc"
1185 | },
1186 | {
1187 | "name": "Opa",
1188 | "value": "opa"
1189 | },
1190 | {
1191 | "name": "Opal",
1192 | "value": "opal"
1193 | },
1194 | {
1195 | "name": "OpenCL",
1196 | "value": "opencl"
1197 | },
1198 | {
1199 | "name": "OpenEdge ABL",
1200 | "value": "openedge-abl"
1201 | },
1202 | {
1203 | "name": "OpenRC runscript",
1204 | "value": "openrc-runscript"
1205 | },
1206 | {
1207 | "name": "OpenSCAD",
1208 | "value": "openscad"
1209 | },
1210 | {
1211 | "name": "OpenType Feature File",
1212 | "value": "opentype-feature-file"
1213 | },
1214 | {
1215 | "name": "Org",
1216 | "value": "org"
1217 | },
1218 | {
1219 | "name": "Ox",
1220 | "value": "ox"
1221 | },
1222 | {
1223 | "name": "Oxygene",
1224 | "value": "oxygene"
1225 | },
1226 | {
1227 | "name": "Oz",
1228 | "value": "oz"
1229 | },
1230 | {
1231 | "name": "P4",
1232 | "value": "p4"
1233 | },
1234 | {
1235 | "name": "Pan",
1236 | "value": "pan"
1237 | },
1238 | {
1239 | "name": "Papyrus",
1240 | "value": "papyrus"
1241 | },
1242 | {
1243 | "name": "Parrot",
1244 | "value": "parrot"
1245 | },
1246 | {
1247 | "name": "Parrot Assembly",
1248 | "value": "parrot-assembly"
1249 | },
1250 | {
1251 | "name": "Parrot Internal Representation",
1252 | "value": "parrot-internal-representation"
1253 | },
1254 | {
1255 | "name": "Pascal",
1256 | "value": "pascal"
1257 | },
1258 | {
1259 | "name": "PAWN",
1260 | "value": "pawn"
1261 | },
1262 | {
1263 | "name": "Pep8",
1264 | "value": "pep8"
1265 | },
1266 | {
1267 | "name": "Perl",
1268 | "value": "perl"
1269 | },
1270 | {
1271 | "name": "Perl 6",
1272 | "value": "perl-6"
1273 | },
1274 | {
1275 | "name": "Pic",
1276 | "value": "pic"
1277 | },
1278 | {
1279 | "name": "Pickle",
1280 | "value": "pickle"
1281 | },
1282 | {
1283 | "name": "PicoLisp",
1284 | "value": "picolisp"
1285 | },
1286 | {
1287 | "name": "PigLatin",
1288 | "value": "piglatin"
1289 | },
1290 | {
1291 | "name": "Pike",
1292 | "value": "pike"
1293 | },
1294 | {
1295 | "name": "PLpgSQL",
1296 | "value": "plpgsql"
1297 | },
1298 | {
1299 | "name": "PLSQL",
1300 | "value": "plsql"
1301 | },
1302 | {
1303 | "name": "Pod",
1304 | "value": "pod"
1305 | },
1306 | {
1307 | "name": "PogoScript",
1308 | "value": "pogoscript"
1309 | },
1310 | {
1311 | "name": "Pony",
1312 | "value": "pony"
1313 | },
1314 | {
1315 | "name": "PostCSS",
1316 | "value": "postcss"
1317 | },
1318 | {
1319 | "name": "PostScript",
1320 | "value": "postscript"
1321 | },
1322 | {
1323 | "name": "POV-Ray SDL",
1324 | "value": "pov-ray-sdl"
1325 | },
1326 | {
1327 | "name": "PowerBuilder",
1328 | "value": "powerbuilder"
1329 | },
1330 | {
1331 | "name": "PowerShell",
1332 | "value": "powershell"
1333 | },
1334 | {
1335 | "name": "Processing",
1336 | "value": "processing"
1337 | },
1338 | {
1339 | "name": "Prolog",
1340 | "value": "prolog"
1341 | },
1342 | {
1343 | "name": "Propeller Spin",
1344 | "value": "propeller-spin"
1345 | },
1346 | {
1347 | "name": "Protocol Buffer",
1348 | "value": "protocol-buffer"
1349 | },
1350 | {
1351 | "name": "Public Key",
1352 | "value": "public-key"
1353 | },
1354 | {
1355 | "name": "Pug",
1356 | "value": "pug"
1357 | },
1358 | {
1359 | "name": "Puppet",
1360 | "value": "puppet"
1361 | },
1362 | {
1363 | "name": "Pure Data",
1364 | "value": "pure-data"
1365 | },
1366 | {
1367 | "name": "PureBasic",
1368 | "value": "purebasic"
1369 | },
1370 | {
1371 | "name": "PureScript",
1372 | "value": "purescript"
1373 | },
1374 | {
1375 | "name": "Python console",
1376 | "value": "python-console"
1377 | },
1378 | {
1379 | "name": "Python traceback",
1380 | "value": "python-traceback"
1381 | },
1382 | {
1383 | "name": "QMake",
1384 | "value": "qmake"
1385 | },
1386 | {
1387 | "name": "QML",
1388 | "value": "qml"
1389 | },
1390 | {
1391 | "name": "R",
1392 | "value": "r"
1393 | },
1394 | {
1395 | "name": "Racket",
1396 | "value": "racket"
1397 | },
1398 | {
1399 | "name": "Ragel",
1400 | "value": "ragel"
1401 | },
1402 | {
1403 | "name": "RAML",
1404 | "value": "raml"
1405 | },
1406 | {
1407 | "name": "Rascal",
1408 | "value": "rascal"
1409 | },
1410 | {
1411 | "name": "Raw token data",
1412 | "value": "raw-token-data"
1413 | },
1414 | {
1415 | "name": "RDoc",
1416 | "value": "rdoc"
1417 | },
1418 | {
1419 | "name": "REALbasic",
1420 | "value": "realbasic"
1421 | },
1422 | {
1423 | "name": "Reason",
1424 | "value": "reason"
1425 | },
1426 | {
1427 | "name": "Rebol",
1428 | "value": "rebol"
1429 | },
1430 | {
1431 | "name": "Red",
1432 | "value": "red"
1433 | },
1434 | {
1435 | "name": "Redcode",
1436 | "value": "redcode"
1437 | },
1438 | {
1439 | "name": "Regular Expression",
1440 | "value": "regular-expression"
1441 | },
1442 | {
1443 | "name": "Ren'Py",
1444 | "value": "ren'py"
1445 | },
1446 | {
1447 | "name": "RenderScript",
1448 | "value": "renderscript"
1449 | },
1450 | {
1451 | "name": "reStructuredText",
1452 | "value": "restructuredtext"
1453 | },
1454 | {
1455 | "name": "REXX",
1456 | "value": "rexx"
1457 | },
1458 | {
1459 | "name": "RHTML",
1460 | "value": "rhtml"
1461 | },
1462 | {
1463 | "name": "Ring",
1464 | "value": "ring"
1465 | },
1466 | {
1467 | "name": "RMarkdown",
1468 | "value": "rmarkdown"
1469 | },
1470 | {
1471 | "name": "RobotFramework",
1472 | "value": "robotframework"
1473 | },
1474 | {
1475 | "name": "Roff",
1476 | "value": "roff"
1477 | },
1478 | {
1479 | "name": "Rouge",
1480 | "value": "rouge"
1481 | },
1482 | {
1483 | "name": "RPC",
1484 | "value": "rpc"
1485 | },
1486 | {
1487 | "name": "RPM Spec",
1488 | "value": "rpm-spec"
1489 | },
1490 | {
1491 | "name": "RUNOFF",
1492 | "value": "runoff"
1493 | },
1494 | {
1495 | "name": "Sage",
1496 | "value": "sage"
1497 | },
1498 | {
1499 | "name": "SaltStack",
1500 | "value": "saltstack"
1501 | },
1502 | {
1503 | "name": "SAS",
1504 | "value": "sas"
1505 | },
1506 | {
1507 | "name": "Sass",
1508 | "value": "sass"
1509 | },
1510 | {
1511 | "name": "Scala",
1512 | "value": "scala"
1513 | },
1514 | {
1515 | "name": "Scaml",
1516 | "value": "scaml"
1517 | },
1518 | {
1519 | "name": "Scheme",
1520 | "value": "scheme"
1521 | },
1522 | {
1523 | "name": "Scilab",
1524 | "value": "scilab"
1525 | },
1526 | {
1527 | "name": "SCSS",
1528 | "value": "scss"
1529 | },
1530 | {
1531 | "name": "sed",
1532 | "value": "sed"
1533 | },
1534 | {
1535 | "name": "Self",
1536 | "value": "self"
1537 | },
1538 | {
1539 | "name": "ShaderLab",
1540 | "value": "shaderlab"
1541 | },
1542 | {
1543 | "name": "ShellSession",
1544 | "value": "shellsession"
1545 | },
1546 | {
1547 | "name": "Shen",
1548 | "value": "shen"
1549 | },
1550 | {
1551 | "name": "Slash",
1552 | "value": "slash"
1553 | },
1554 | {
1555 | "name": "Slim",
1556 | "value": "slim"
1557 | },
1558 | {
1559 | "name": "Smali",
1560 | "value": "smali"
1561 | },
1562 | {
1563 | "name": "Smalltalk",
1564 | "value": "smalltalk"
1565 | },
1566 | {
1567 | "name": "Smarty",
1568 | "value": "smarty"
1569 | },
1570 | {
1571 | "name": "SMT",
1572 | "value": "smt"
1573 | },
1574 | {
1575 | "name": "Solidity",
1576 | "value": "solidity"
1577 | },
1578 | {
1579 | "name": "SourcePawn",
1580 | "value": "sourcepawn"
1581 | },
1582 | {
1583 | "name": "SPARQL",
1584 | "value": "sparql"
1585 | },
1586 | {
1587 | "name": "Spline Font Database",
1588 | "value": "spline-font-database"
1589 | },
1590 | {
1591 | "name": "SQF",
1592 | "value": "sqf"
1593 | },
1594 | {
1595 | "name": "SQL",
1596 | "value": "sql"
1597 | },
1598 | {
1599 | "name": "SQLPL",
1600 | "value": "sqlpl"
1601 | },
1602 | {
1603 | "name": "Squirrel",
1604 | "value": "squirrel"
1605 | },
1606 | {
1607 | "name": "SRecode Template",
1608 | "value": "srecode-template"
1609 | },
1610 | {
1611 | "name": "Stan",
1612 | "value": "stan"
1613 | },
1614 | {
1615 | "name": "Standard ML",
1616 | "value": "standard-ml"
1617 | },
1618 | {
1619 | "name": "Stata",
1620 | "value": "stata"
1621 | },
1622 | {
1623 | "name": "STON",
1624 | "value": "ston"
1625 | },
1626 | {
1627 | "name": "Stylus",
1628 | "value": "stylus"
1629 | },
1630 | {
1631 | "name": "Sublime Text Config",
1632 | "value": "sublime-text-config"
1633 | },
1634 | {
1635 | "name": "SubRip Text",
1636 | "value": "subrip-text"
1637 | },
1638 | {
1639 | "name": "SugarSS",
1640 | "value": "sugarss"
1641 | },
1642 | {
1643 | "name": "SuperCollider",
1644 | "value": "supercollider"
1645 | },
1646 | {
1647 | "name": "SVG",
1648 | "value": "svg"
1649 | },
1650 | {
1651 | "name": "Swift",
1652 | "value": "swift"
1653 | },
1654 | {
1655 | "name": "SystemVerilog",
1656 | "value": "systemverilog"
1657 | },
1658 | {
1659 | "name": "Tcl",
1660 | "value": "tcl"
1661 | },
1662 | {
1663 | "name": "Tcsh",
1664 | "value": "tcsh"
1665 | },
1666 | {
1667 | "name": "Tea",
1668 | "value": "tea"
1669 | },
1670 | {
1671 | "name": "Terra",
1672 | "value": "terra"
1673 | },
1674 | {
1675 | "name": "TeX",
1676 | "value": "tex"
1677 | },
1678 | {
1679 | "name": "Text",
1680 | "value": "text"
1681 | },
1682 | {
1683 | "name": "Textile",
1684 | "value": "textile"
1685 | },
1686 | {
1687 | "name": "Thrift",
1688 | "value": "thrift"
1689 | },
1690 | {
1691 | "name": "TI Program",
1692 | "value": "ti-program"
1693 | },
1694 | {
1695 | "name": "TLA",
1696 | "value": "tla"
1697 | },
1698 | {
1699 | "name": "TOML",
1700 | "value": "toml"
1701 | },
1702 | {
1703 | "name": "Turing",
1704 | "value": "turing"
1705 | },
1706 | {
1707 | "name": "Turtle",
1708 | "value": "turtle"
1709 | },
1710 | {
1711 | "name": "Twig",
1712 | "value": "twig"
1713 | },
1714 | {
1715 | "name": "TXL",
1716 | "value": "txl"
1717 | },
1718 | {
1719 | "name": "Type Language",
1720 | "value": "type-language"
1721 | },
1722 | {
1723 | "name": "TypeScript",
1724 | "value": "typescript"
1725 | },
1726 | {
1727 | "name": "Unified Parallel C",
1728 | "value": "unified-parallel-c"
1729 | },
1730 | {
1731 | "name": "Unity3D Asset",
1732 | "value": "unity3d-asset"
1733 | },
1734 | {
1735 | "name": "Unix Assembly",
1736 | "value": "unix-assembly"
1737 | },
1738 | {
1739 | "name": "Uno",
1740 | "value": "uno"
1741 | },
1742 | {
1743 | "name": "UnrealScript",
1744 | "value": "unrealscript"
1745 | },
1746 | {
1747 | "name": "UrWeb",
1748 | "value": "urweb"
1749 | },
1750 | {
1751 | "name": "Vala",
1752 | "value": "vala"
1753 | },
1754 | {
1755 | "name": "VCL",
1756 | "value": "vcl"
1757 | },
1758 | {
1759 | "name": "Verilog",
1760 | "value": "verilog"
1761 | },
1762 | {
1763 | "name": "VHDL",
1764 | "value": "vhdl"
1765 | },
1766 | {
1767 | "name": "Vim script",
1768 | "value": "vim-script"
1769 | },
1770 | {
1771 | "name": "Visual Basic",
1772 | "value": "visual-basic"
1773 | },
1774 | {
1775 | "name": "Volt",
1776 | "value": "volt"
1777 | },
1778 | {
1779 | "name": "Vue",
1780 | "value": "vue"
1781 | },
1782 | {
1783 | "name": "Wavefront Material",
1784 | "value": "wavefront-material"
1785 | },
1786 | {
1787 | "name": "Wavefront Object",
1788 | "value": "wavefront-object"
1789 | },
1790 | {
1791 | "name": "wdl",
1792 | "value": "wdl"
1793 | },
1794 | {
1795 | "name": "Web Ontology Language",
1796 | "value": "web-ontology-language"
1797 | },
1798 | {
1799 | "name": "WebAssembly",
1800 | "value": "webassembly"
1801 | },
1802 | {
1803 | "name": "WebIDL",
1804 | "value": "webidl"
1805 | },
1806 | {
1807 | "name": "wisp",
1808 | "value": "wisp"
1809 | },
1810 | {
1811 | "name": "World of Warcraft Addon Data",
1812 | "value": "world-of-warcraft-addon-data"
1813 | },
1814 | {
1815 | "name": "X10",
1816 | "value": "x10"
1817 | },
1818 | {
1819 | "name": "xBase",
1820 | "value": "xbase"
1821 | },
1822 | {
1823 | "name": "XC",
1824 | "value": "xc"
1825 | },
1826 | {
1827 | "name": "XCompose",
1828 | "value": "xcompose"
1829 | },
1830 | {
1831 | "name": "XML",
1832 | "value": "xml"
1833 | },
1834 | {
1835 | "name": "Xojo",
1836 | "value": "xojo"
1837 | },
1838 | {
1839 | "name": "XPages",
1840 | "value": "xpages"
1841 | },
1842 | {
1843 | "name": "XPM",
1844 | "value": "xpm"
1845 | },
1846 | {
1847 | "name": "XProc",
1848 | "value": "xproc"
1849 | },
1850 | {
1851 | "name": "XQuery",
1852 | "value": "xquery"
1853 | },
1854 | {
1855 | "name": "XS",
1856 | "value": "xs"
1857 | },
1858 | {
1859 | "name": "XSLT",
1860 | "value": "xslt"
1861 | },
1862 | {
1863 | "name": "Xtend",
1864 | "value": "xtend"
1865 | },
1866 | {
1867 | "name": "Yacc",
1868 | "value": "yacc"
1869 | },
1870 | {
1871 | "name": "YAML",
1872 | "value": "yaml"
1873 | },
1874 | {
1875 | "name": "YANG",
1876 | "value": "yang"
1877 | },
1878 | {
1879 | "name": "YARA",
1880 | "value": "yara"
1881 | },
1882 | {
1883 | "name": "Zephir",
1884 | "value": "zephir"
1885 | },
1886 | {
1887 | "name": "Zimpl",
1888 | "value": "zimpl"
1889 | }
1890 | ]
1891 |
--------------------------------------------------------------------------------
/src/lib/github/spoken-languages.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "All Languages",
4 | "value": ""
5 | },
6 | {
7 | "name": "English",
8 | "value": "en"
9 | },
10 | {
11 | "name": "Chinese",
12 | "value": "zh"
13 | },
14 | {
15 | "name": "Russian",
16 | "value": "ru"
17 | },
18 | {
19 | "name": "German",
20 | "value": "de"
21 | },
22 | {
23 | "name": "French",
24 | "value": "fr"
25 | },
26 | {
27 | "name": "Japanese",
28 | "value": "ja"
29 | },
30 | {
31 | "name": "Abkhazian",
32 | "value": "ab"
33 | },
34 | {
35 | "name": "Afar",
36 | "value": "aa"
37 | },
38 | {
39 | "name": "Afrikaans",
40 | "value": "af"
41 | },
42 | {
43 | "name": "Akan",
44 | "value": "ak"
45 | },
46 | {
47 | "name": "Albanian",
48 | "value": "sq"
49 | },
50 | {
51 | "name": "Amharic",
52 | "value": "am"
53 | },
54 | {
55 | "name": "Arabic",
56 | "value": "ar"
57 | },
58 | {
59 | "name": "Aragonese",
60 | "value": "an"
61 | },
62 | {
63 | "name": "Armenian",
64 | "value": "hy"
65 | },
66 | {
67 | "name": "Assamese",
68 | "value": "as"
69 | },
70 | {
71 | "name": "Avaric",
72 | "value": "av"
73 | },
74 | {
75 | "name": "Avestan",
76 | "value": "ae"
77 | },
78 | {
79 | "name": "Aymara",
80 | "value": "ay"
81 | },
82 | {
83 | "name": "Azerbaijani",
84 | "value": "az"
85 | },
86 | {
87 | "name": "Bambara",
88 | "value": "bm"
89 | },
90 | {
91 | "name": "Bashkir",
92 | "value": "ba"
93 | },
94 | {
95 | "name": "Basque",
96 | "value": "eu"
97 | },
98 | {
99 | "name": "Belarusian",
100 | "value": "be"
101 | },
102 | {
103 | "name": "Bengali",
104 | "value": "bn"
105 | },
106 | {
107 | "name": "Bihari languages",
108 | "value": "bh"
109 | },
110 | {
111 | "name": "Bislama",
112 | "value": "bi"
113 | },
114 | {
115 | "name": "Bosnian",
116 | "value": "bs"
117 | },
118 | {
119 | "name": "Breton",
120 | "value": "br"
121 | },
122 | {
123 | "name": "Bulgarian",
124 | "value": "bg"
125 | },
126 | {
127 | "name": "Burmese",
128 | "value": "my"
129 | },
130 | {
131 | "name": "Catalan, Valencian",
132 | "value": "ca"
133 | },
134 | {
135 | "name": "Chamorro",
136 | "value": "ch"
137 | },
138 | {
139 | "name": "Chechen",
140 | "value": "ce"
141 | },
142 | {
143 | "name": "Chichewa, Chewa, Nyanja",
144 | "value": "ny"
145 | },
146 | {
147 | "name": "Chuvash",
148 | "value": "cv"
149 | },
150 | {
151 | "name": "Cornish",
152 | "value": "kw"
153 | },
154 | {
155 | "name": "Corsican",
156 | "value": "co"
157 | },
158 | {
159 | "name": "Cree",
160 | "value": "cr"
161 | },
162 | {
163 | "name": "Croatian",
164 | "value": "hr"
165 | },
166 | {
167 | "name": "Czech",
168 | "value": "cs"
169 | },
170 | {
171 | "name": "Danish",
172 | "value": "da"
173 | },
174 | {
175 | "name": "Divehi, Dhivehi, Maldivian",
176 | "value": "dv"
177 | },
178 | {
179 | "name": "Dutch, Flemish",
180 | "value": "nl"
181 | },
182 | {
183 | "name": "Dzongkha",
184 | "value": "dz"
185 | },
186 | {
187 | "name": "Esperanto",
188 | "value": "eo"
189 | },
190 | {
191 | "name": "Estonian",
192 | "value": "et"
193 | },
194 | {
195 | "name": "Ewe",
196 | "value": "ee"
197 | },
198 | {
199 | "name": "Faroese",
200 | "value": "fo"
201 | },
202 | {
203 | "name": "Fijian",
204 | "value": "fj"
205 | },
206 | {
207 | "name": "Finnish",
208 | "value": "fi"
209 | },
210 | {
211 | "name": "Fulah",
212 | "value": "ff"
213 | },
214 | {
215 | "name": "Galician",
216 | "value": "gl"
217 | },
218 | {
219 | "name": "Georgian",
220 | "value": "ka"
221 | },
222 | {
223 | "name": "Greek, Modern",
224 | "value": "el"
225 | },
226 | {
227 | "name": "Guarani",
228 | "value": "gn"
229 | },
230 | {
231 | "name": "Gujarati",
232 | "value": "gu"
233 | },
234 | {
235 | "name": "Haitian, Haitian Creole",
236 | "value": "ht"
237 | },
238 | {
239 | "name": "Hausa",
240 | "value": "ha"
241 | },
242 | {
243 | "name": "Hebrew",
244 | "value": "he"
245 | },
246 | {
247 | "name": "Herero",
248 | "value": "hz"
249 | },
250 | {
251 | "name": "Hindi",
252 | "value": "hi"
253 | },
254 | {
255 | "name": "Hiri Motu",
256 | "value": "ho"
257 | },
258 | {
259 | "name": "Hungarian",
260 | "value": "hu"
261 | },
262 | {
263 | "name": "Interlingua (International Auxil...",
264 | "value": "ia"
265 | },
266 | {
267 | "name": "Indonesian",
268 | "value": "id"
269 | },
270 | {
271 | "name": "Interlingue, Occidental",
272 | "value": "ie"
273 | },
274 | {
275 | "name": "Irish",
276 | "value": "ga"
277 | },
278 | {
279 | "name": "Igbo",
280 | "value": "ig"
281 | },
282 | {
283 | "name": "Inupiaq",
284 | "value": "ik"
285 | },
286 | {
287 | "name": "Ido",
288 | "value": "io"
289 | },
290 | {
291 | "name": "Icelandic",
292 | "value": "is"
293 | },
294 | {
295 | "name": "Italian",
296 | "value": "it"
297 | },
298 | {
299 | "name": "Inuktitut",
300 | "value": "iu"
301 | },
302 | {
303 | "name": "Javanese",
304 | "value": "jv"
305 | },
306 | {
307 | "name": "Kalaallisut, Greenlandic",
308 | "value": "kl"
309 | },
310 | {
311 | "name": "Kannada",
312 | "value": "kn"
313 | },
314 | {
315 | "name": "Kanuri",
316 | "value": "kr"
317 | },
318 | {
319 | "name": "Kashmiri",
320 | "value": "ks"
321 | },
322 | {
323 | "name": "Kazakh",
324 | "value": "kk"
325 | },
326 | {
327 | "name": "Central Khmer",
328 | "value": "km"
329 | },
330 | {
331 | "name": "Kikuyu, Gikuyu",
332 | "value": "ki"
333 | },
334 | {
335 | "name": "Kinyarwanda",
336 | "value": "rw"
337 | },
338 | {
339 | "name": "Kirghiz, Kyrgyz",
340 | "value": "ky"
341 | },
342 | {
343 | "name": "Komi",
344 | "value": "kv"
345 | },
346 | {
347 | "name": "Kongo",
348 | "value": "kg"
349 | },
350 | {
351 | "name": "Korean",
352 | "value": "ko"
353 | },
354 | {
355 | "name": "Kurdish",
356 | "value": "ku"
357 | },
358 | {
359 | "name": "Kuanyama, Kwanyama",
360 | "value": "kj"
361 | },
362 | {
363 | "name": "Latin",
364 | "value": "la"
365 | },
366 | {
367 | "name": "Luxembourgish, Letzeburgesch",
368 | "value": "lb"
369 | },
370 | {
371 | "name": "Ganda",
372 | "value": "lg"
373 | },
374 | {
375 | "name": "Limburgan, Limburger, Limburgish",
376 | "value": "li"
377 | },
378 | {
379 | "name": "Lingala",
380 | "value": "ln"
381 | },
382 | {
383 | "name": "Lao",
384 | "value": "lo"
385 | },
386 | {
387 | "name": "Lithuanian",
388 | "value": "lt"
389 | },
390 | {
391 | "name": "Luba-Katanga",
392 | "value": "lu"
393 | },
394 | {
395 | "name": "Latvian",
396 | "value": "lv"
397 | },
398 | {
399 | "name": "Manx",
400 | "value": "gv"
401 | },
402 | {
403 | "name": "Macedonian",
404 | "value": "mk"
405 | },
406 | {
407 | "name": "Malagasy",
408 | "value": "mg"
409 | },
410 | {
411 | "name": "Malay",
412 | "value": "ms"
413 | },
414 | {
415 | "name": "Malayalam",
416 | "value": "ml"
417 | },
418 | {
419 | "name": "Maltese",
420 | "value": "mt"
421 | },
422 | {
423 | "name": "Maori",
424 | "value": "mi"
425 | },
426 | {
427 | "name": "Marathi",
428 | "value": "mr"
429 | },
430 | {
431 | "name": "Marshallese",
432 | "value": "mh"
433 | },
434 | {
435 | "name": "Mongolian",
436 | "value": "mn"
437 | },
438 | {
439 | "name": "Nauru",
440 | "value": "na"
441 | },
442 | {
443 | "name": "Navajo, Navaho",
444 | "value": "nv"
445 | },
446 | {
447 | "name": "North Ndebele",
448 | "value": "nd"
449 | },
450 | {
451 | "name": "Nepali",
452 | "value": "ne"
453 | },
454 | {
455 | "name": "Ndonga",
456 | "value": "ng"
457 | },
458 | {
459 | "name": "Norwegian Bokmål",
460 | "value": "nb"
461 | },
462 | {
463 | "name": "Norwegian Nynorsk",
464 | "value": "nn"
465 | },
466 | {
467 | "name": "Norwegian",
468 | "value": "no"
469 | },
470 | {
471 | "name": "Sichuan Yi, Nuosu",
472 | "value": "ii"
473 | },
474 | {
475 | "name": "South Ndebele",
476 | "value": "nr"
477 | },
478 | {
479 | "name": "Occitan",
480 | "value": "oc"
481 | },
482 | {
483 | "name": "Ojibwa",
484 | "value": "oj"
485 | },
486 | {
487 | "name": "Church Slavic, Old Slavonic, Chu...",
488 | "value": "cu"
489 | },
490 | {
491 | "name": "Oromo",
492 | "value": "om"
493 | },
494 | {
495 | "name": "Oriya",
496 | "value": "or"
497 | },
498 | {
499 | "name": "Ossetian, Ossetic",
500 | "value": "os"
501 | },
502 | {
503 | "name": "Punjabi, Panjabi",
504 | "value": "pa"
505 | },
506 | {
507 | "name": "Pali",
508 | "value": "pi"
509 | },
510 | {
511 | "name": "Persian",
512 | "value": "fa"
513 | },
514 | {
515 | "name": "Polish",
516 | "value": "pl"
517 | },
518 | {
519 | "name": "Pashto, Pushto",
520 | "value": "ps"
521 | },
522 | {
523 | "name": "Portuguese",
524 | "value": "pt"
525 | },
526 | {
527 | "name": "Quechua",
528 | "value": "qu"
529 | },
530 | {
531 | "name": "Romansh",
532 | "value": "rm"
533 | },
534 | {
535 | "name": "Rundi",
536 | "value": "rn"
537 | },
538 | {
539 | "name": "Romanian, Moldavian, Moldovan",
540 | "value": "ro"
541 | },
542 | {
543 | "name": "Sanskrit",
544 | "value": "sa"
545 | },
546 | {
547 | "name": "Sardinian",
548 | "value": "sc"
549 | },
550 | {
551 | "name": "Sindhi",
552 | "value": "sd"
553 | },
554 | {
555 | "name": "Northern Sami",
556 | "value": "se"
557 | },
558 | {
559 | "name": "Samoan",
560 | "value": "sm"
561 | },
562 | {
563 | "name": "Sango",
564 | "value": "sg"
565 | },
566 | {
567 | "name": "Serbian",
568 | "value": "sr"
569 | },
570 | {
571 | "name": "Gaelic, Scottish Gaelic",
572 | "value": "gd"
573 | },
574 | {
575 | "name": "Shona",
576 | "value": "sn"
577 | },
578 | {
579 | "name": "Sinhala, Sinhalese",
580 | "value": "si"
581 | },
582 | {
583 | "name": "Slovak",
584 | "value": "sk"
585 | },
586 | {
587 | "name": "Slovenian",
588 | "value": "sl"
589 | },
590 | {
591 | "name": "Somali",
592 | "value": "so"
593 | },
594 | {
595 | "name": "Southern Sotho",
596 | "value": "st"
597 | },
598 | {
599 | "name": "Spanish, Castilian",
600 | "value": "es"
601 | },
602 | {
603 | "name": "Sundanese",
604 | "value": "su"
605 | },
606 | {
607 | "name": "Swahili",
608 | "value": "sw"
609 | },
610 | {
611 | "name": "Swati",
612 | "value": "ss"
613 | },
614 | {
615 | "name": "Swedish",
616 | "value": "sv"
617 | },
618 | {
619 | "name": "Tamil",
620 | "value": "ta"
621 | },
622 | {
623 | "name": "Telugu",
624 | "value": "te"
625 | },
626 | {
627 | "name": "Tajik",
628 | "value": "tg"
629 | },
630 | {
631 | "name": "Thai",
632 | "value": "th"
633 | },
634 | {
635 | "name": "Tigrinya",
636 | "value": "ti"
637 | },
638 | {
639 | "name": "Tibetan",
640 | "value": "bo"
641 | },
642 | {
643 | "name": "Turkmen",
644 | "value": "tk"
645 | },
646 | {
647 | "name": "Tagalog",
648 | "value": "tl"
649 | },
650 | {
651 | "name": "Tswana",
652 | "value": "tn"
653 | },
654 | {
655 | "name": "Tonga (Tonga Islands)",
656 | "value": "to"
657 | },
658 | {
659 | "name": "Turkish",
660 | "value": "tr"
661 | },
662 | {
663 | "name": "Tsonga",
664 | "value": "ts"
665 | },
666 | {
667 | "name": "Tatar",
668 | "value": "tt"
669 | },
670 | {
671 | "name": "Twi",
672 | "value": "tw"
673 | },
674 | {
675 | "name": "Tahitian",
676 | "value": "ty"
677 | },
678 | {
679 | "name": "Uighur, Uyghur",
680 | "value": "ug"
681 | },
682 | {
683 | "name": "Ukrainian",
684 | "value": "uk"
685 | },
686 | {
687 | "name": "Urdu",
688 | "value": "ur"
689 | },
690 | {
691 | "name": "Uzbek",
692 | "value": "uz"
693 | },
694 | {
695 | "name": "Venda",
696 | "value": "ve"
697 | },
698 | {
699 | "name": "Vietnamese",
700 | "value": "vi"
701 | },
702 | {
703 | "name": "Volapük",
704 | "value": "vo"
705 | },
706 | {
707 | "name": "Walloon",
708 | "value": "wa"
709 | },
710 | {
711 | "name": "Welsh",
712 | "value": "cy"
713 | },
714 | {
715 | "name": "Wolof",
716 | "value": "wo"
717 | },
718 | {
719 | "name": "Western Frisian",
720 | "value": "fy"
721 | },
722 | {
723 | "name": "Xhosa",
724 | "value": "xh"
725 | },
726 | {
727 | "name": "Yiddish",
728 | "value": "yi"
729 | },
730 | {
731 | "name": "Yoruba",
732 | "value": "yo"
733 | },
734 | {
735 | "name": "Zhuang, Chuang",
736 | "value": "za"
737 | },
738 | {
739 | "name": "Zulu",
740 | "value": "zu"
741 | }
742 | ]
743 |
--------------------------------------------------------------------------------
/src/lib/runtime.js:
--------------------------------------------------------------------------------
1 | export const isRunningExtension = (window.chrome &&
2 | window.chrome.runtime &&
3 | window.chrome.runtime.id) || false;
4 |
5 | export const isRunningChromeExtension = isRunningExtension;
6 |
7 | export function isMobileDevice() {
8 | return (typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1);
9 | };
10 |
--------------------------------------------------------------------------------
/src/lib/storages.js:
--------------------------------------------------------------------------------
1 |
2 | import reduxLocalStorage from 'redux-persist/lib/storage';
3 | import createChromeStorage from 'redux-persist-chrome-storage'
4 |
5 | import {isRunningChromeExtension} from 'lib/runtime';
6 |
7 | export const preferredStorage = isRunningChromeExtension ? createChromeStorage(window.chrome, 'sync') : reduxLocalStorage;
8 |
--------------------------------------------------------------------------------
/src/redux/accounts/actions.js:
--------------------------------------------------------------------------------
1 | // import ReactGA from 'react-ga';
2 | import {
3 | UPDATE_GITHUB_ACCESS_TOKEN
4 | } from "./types";
5 |
6 | export const updateGitHubAccessToken = function (token) {
7 | return dispatch => {
8 | dispatch({
9 | type: UPDATE_GITHUB_ACCESS_TOKEN,
10 | payload: token
11 | });
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/src/redux/accounts/reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | UPDATE_GITHUB_ACCESS_TOKEN
3 | } from './types';
4 |
5 | const initialState = {
6 | GitHubAccessToken: null
7 | };
8 |
9 | export default function reducer(state = initialState, action) {
10 | switch (action.type) {
11 | case UPDATE_GITHUB_ACCESS_TOKEN:
12 | return {
13 | ...state,
14 | GitHubAccessToken: action.payload
15 | };
16 | default:
17 | return state;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/redux/accounts/types.js:
--------------------------------------------------------------------------------
1 | export const UPDATE_GITHUB_ACCESS_TOKEN = 'UPDATE_GITHUB_ACCESS_TOKEN';
2 |
--------------------------------------------------------------------------------
/src/redux/preference/actions.js:
--------------------------------------------------------------------------------
1 | import ReactGA from 'react-ga';
2 | import {
3 | UPDATE_DATE_TYPE,
4 | UPDATE_LANGUAGE,
5 | UPDATE_VIEW_TYPE,
6 | SET_COLOR_THEME,
7 | SET_WHETHER_OCCUPY_NEWTAB,
8 | } from "./types";
9 |
10 | export const updateViewType = function (viewType = 'grid') {
11 | return dispatch => {
12 | dispatch({
13 | type: UPDATE_VIEW_TYPE,
14 | payload: viewType
15 | });
16 | ReactGA.event({
17 | category: 'Preference',
18 | label: 'Set Trending ViewType',
19 | action: `Trending ViewType Set to ${viewType}`
20 | });
21 | };
22 | };
23 |
24 | export const updateLanguage = function (language) {
25 | return dispatch => {
26 | dispatch({
27 | type: UPDATE_LANGUAGE,
28 | payload: language
29 | });
30 | ReactGA.event({
31 | category: 'Preference',
32 | label: 'Set Trending Language',
33 | action: `Trending Language Set to ${language || "All"}`
34 | });
35 | };
36 | };
37 |
38 | export const updateDateJump = function (dateJump) {
39 | return dispatch => {
40 | dispatch({
41 | type: UPDATE_DATE_TYPE,
42 | payload: dateJump
43 | });
44 | ReactGA.event({
45 | category: 'Preference',
46 | label: 'Set Trending Period',
47 | action: `Trending Period Set to ${dateJump}`
48 | });
49 | };
50 | };
51 |
52 |
53 | export function setColorTheme(theme) {
54 | return dispatch => {
55 | dispatch({
56 | type: SET_COLOR_THEME,
57 | payload: theme
58 | });
59 | ReactGA.event({
60 | category: 'Preference',
61 | label: 'Set Color Theme',
62 | action: `Color Theme Set to ${theme}`
63 | });
64 | };
65 | }
66 |
67 | export function setWhetherOccupyNewTab(b) {
68 | return dispatch => {
69 | dispatch({
70 | type: SET_WHETHER_OCCUPY_NEWTAB,
71 | payload: b
72 | });
73 | ReactGA.event({
74 | category: 'Preference',
75 | label: 'Set Whether Occupy New Tab',
76 | action: b? 'OK to Occupy New Tab' : 'Not Occupy New Tab'
77 | });
78 | };
79 | }
80 |
--------------------------------------------------------------------------------
/src/redux/preference/reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | UPDATE_DATE_TYPE,
3 | UPDATE_LANGUAGE,
4 | UPDATE_VIEW_TYPE,
5 | SET_COLOR_THEME,
6 | SET_WHETHER_OCCUPY_NEWTAB,
7 | } from './types';
8 |
9 | const initialState = {
10 | whether_occupy_newtab: true,
11 | theme: 'system',
12 |
13 | // used for repo lists
14 | viewType: 'grid',
15 | dateJump: 'week',
16 | language: '',
17 | };
18 |
19 | export default function reducer(state = initialState, action) {
20 | switch (action.type) {
21 | case UPDATE_DATE_TYPE:
22 | return {
23 | ...state,
24 | dateJump: action.payload
25 | };
26 | case UPDATE_VIEW_TYPE:
27 | return {
28 | ...state,
29 | viewType: action.payload
30 | };
31 | case UPDATE_LANGUAGE:
32 | return {
33 | ...state,
34 | language: action.payload
35 | };
36 | case SET_COLOR_THEME:
37 | return {
38 | ...state,
39 | theme: action.payload
40 | };
41 | case SET_WHETHER_OCCUPY_NEWTAB:
42 | return {
43 | ...state,
44 | whether_occupy_newtab: action.payload
45 | };
46 | default:
47 | return state;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/redux/preference/types.js:
--------------------------------------------------------------------------------
1 | export const UPDATE_VIEW_TYPE = 'UPDATE_VIEW_TYPE';
2 | export const UPDATE_DATE_TYPE = 'UPDATE_DATE_TYPE';
3 | export const UPDATE_LANGUAGE = 'UPDATE_LANGUAGE';
4 | export const SET_COLOR_THEME = 'SET_COLOR_THEME';
5 | export const SET_WHETHER_OCCUPY_NEWTAB = 'SET_WHETHER_OCCUPY_NEWTAB';
6 |
--------------------------------------------------------------------------------
/src/redux/reducers.js:
--------------------------------------------------------------------------------
1 | export {default as preference} from './preference/reducer';
2 | export {default as userData} from './user-data/reducer';
3 | export {default as accounts} from './accounts/reducer';
4 |
--------------------------------------------------------------------------------
/src/redux/user-data/actions.js:
--------------------------------------------------------------------------------
1 | // import ReactGA from 'react-ga';
2 | import {
3 | DISMISS_USER_TIP,
4 | } from "./types";
5 |
6 |
7 | export function dismissUserTip(tipID) {
8 | return dispatch => {
9 | dispatch({
10 | type: DISMISS_USER_TIP,
11 | payload: tipID
12 | });
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/src/redux/user-data/reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | DISMISS_USER_TIP,
3 | EMPTY_DISMISSED_USER_TIPS,
4 | } from './types';
5 |
6 | const initialState = {
7 | dismissedUserTips: [],
8 | firstSeenTime: (new Date()).toISOString(),
9 | };
10 |
11 | export default function reducer(state = initialState, action) {
12 | switch (action.type) {
13 | case DISMISS_USER_TIP:
14 | let dismissedUserTips = [...state.dismissedUserTips] || [];
15 | if (!dismissedUserTips.includes(action.payload)) {
16 | dismissedUserTips.push(action.payload)
17 | if (dismissedUserTips.length > 100) {
18 | dismissedUserTips.shift()
19 | }
20 | }
21 | return {
22 | ...state,
23 | dismissedUserTips
24 | };
25 | case EMPTY_DISMISSED_USER_TIPS:
26 | return {
27 | ...state,
28 | dismissedUserTips: []
29 | };
30 | default:
31 | return state;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/redux/user-data/types.js:
--------------------------------------------------------------------------------
1 | export const DISMISS_USER_TIP = 'DISMISS_USER_TIP';
2 | export const EMPTY_DISMISSED_USER_TIPS = 'EMPTY_DISMISSED_USER_TIPS';
3 |
--------------------------------------------------------------------------------
/src/routes/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Switch } from 'react-router-dom';
3 |
4 | import FeedContainer from 'containers/feed';
5 | import CommentsContainer from 'containers/comments';
6 | import withTracker from './with-tracker';
7 |
8 | const FeedContainerWithGATracker = withTracker(FeedContainer);
9 |
10 | const AppRoutes = () => {
11 | return (
12 |
13 |
14 | {/* evaluate HOC wrapper in below can make a rerender after theme change */}
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default AppRoutes;
22 |
--------------------------------------------------------------------------------
/src/routes/with-tracker.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import ReactGA from "react-ga";
3 |
4 | export default (WrappedComponent, options = {}) => {
5 | const trackPage = page => {
6 | ReactGA.set({
7 | page,
8 | ...options
9 | });
10 | ReactGA.pageview(page);
11 | };
12 |
13 | const WithGATracker = props => {
14 | useEffect(() => trackPage(props.location.pathname + document.location.search), [
15 | props.location.pathname
16 | ]);
17 |
18 | return ;
19 | };
20 |
21 | return WithGATracker;
22 | };
23 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import { combineReducers, applyMiddleware, createStore } from 'redux';
2 |
3 | import { persistReducer, persistStore } from 'redux-persist';
4 | import thunk from 'redux-thunk';
5 | import { composeWithDevTools } from 'redux-devtools-extension';
6 | import reduxLocalStorage from 'redux-persist/lib/storage';
7 | import createChromeStorage from 'redux-persist-chrome-storage'
8 | import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';
9 |
10 | import {isRunningChromeExtension} from 'lib/runtime';
11 | import * as reducers from './redux/reducers';
12 |
13 | const preferredStorage = isRunningChromeExtension ? createChromeStorage(window.chrome, 'sync') : reduxLocalStorage;
14 |
15 | const rootReducer = combineReducers({
16 | 'accounts': reducers.accounts,
17 | 'preference': reducers.preference,
18 | 'userData': persistReducer(
19 | {
20 | key: 'hitup:user-data',
21 | storage: preferredStorage,
22 | timeout: null,
23 | blacklist: [],
24 | stateReconciler: autoMergeLevel2,
25 | },
26 | reducers.userData
27 | ),
28 | });
29 |
30 | const persistedRootReducer = persistReducer(
31 | {
32 | key: 'hitup:root',
33 | storage: preferredStorage,
34 | timeout: null,
35 | whitelist: ['preference', 'accounts'],
36 | // blacklist: ['userData'],
37 | stateReconciler: autoMergeLevel2,
38 | },
39 | rootReducer,
40 | );
41 |
42 | export const store = createStore(persistedRootReducer, composeWithDevTools(applyMiddleware(thunk)));
43 | export const persist = persistStore(store);
44 |
--------------------------------------------------------------------------------
/src/theme.scss:
--------------------------------------------------------------------------------
1 | @import '~bootstrap/scss/functions';
2 | @import '~bootstrap/scss/variables';
3 |
4 | @mixin light-theme {
5 | --background-primary-color: #f4f4f4;
6 | --background-secondary-color: #fff;
7 |
8 | --text-main-color: #{$gray-700}; // main text, e..g repo description
9 | --text-head-color: #{$gray-900};
10 | --text-secondary-color: #{$gray-600};
11 | --text-faded-color: #{$gray-400};
12 |
13 | --box-shadow-color: rgba(0, 0, 0, 0.2);
14 | --border-primary-color: #e8e8e8;
15 |
16 | --top-nav-background-color: #fff;
17 | --top-nav-border-color: #e8e8e8;
18 |
19 | --primary-button-background-color: #fff;
20 | --primary-button-border-color: #fff;
21 | }
22 |
23 | @mixin dark-theme {
24 | --background-primary-color: #090809;
25 | --background-secondary-color: #131313;
26 |
27 | --text-main-color: #{$gray-500}; // main text, e..g repo description
28 | --text-head-color: #{$gray-300};
29 | --text-secondary-color: #{$gray-600};
30 | --text-faded-color: #{$gray-700};
31 |
32 | --box-shadow-color: rgba(0, 0, 0, 0.2);
33 | --border-primary-color: #313131;
34 |
35 | --top-nav-background-color: #010101;
36 | --top-nav-border-color: var(--border-primary-color);
37 |
38 | --primary-button-background-color: #131313;
39 | --primary-button-border-color: #131313;
40 | }
41 |
42 | @mixin dark-theme-blue {
43 | --background-primary-color: #01385c;
44 | --background-secondary-color: #131313;
45 |
46 | --text-main-color: #{$gray-500}; // main text, e..g repo description
47 | --text-head-color: #{$gray-300};
48 | --text-secondary-color: #{$gray-600};
49 | --text-faded-color: #{$gray-700};
50 |
51 | --box-shadow-color: rgba(0, 0, 0, 0.2);
52 | --border-primary-color: #547891;
53 |
54 | --top-nav-background-color: #010101;
55 | --top-nav-border-color: var(--border-primary-color);
56 |
57 | --primary-button-background-color: #071720;
58 | --primary-button-border-color: #071720;
59 | }
60 |
61 | .theme-light {
62 | @include light-theme;
63 | }
64 |
65 | .theme-dark {
66 | @include dark-theme;
67 | }
68 |
69 | .theme-dark-blue {
70 | @include dark-theme-blue;
71 | }
72 |
73 | :root {
74 | color-scheme: dark;
75 | @include dark-theme-blue;
76 |
77 | @media (prefers-color-scheme: light) {
78 | color-scheme: light;
79 | @include light-theme;
80 | }
81 | }
--------------------------------------------------------------------------------
/src/variables.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonderbeyond/HitUP/765d99332b9fc14df6e2d2554a33bcb0901d8696/src/variables.scss
--------------------------------------------------------------------------------