├── .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 | 18 |
19 |
20 | 36 |
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 | HitUP 3 |
HitUP 4 |

5 | 6 |

7 | 8 | contributions 9 | 10 | 11 | version 12 | 13 | 14 | rating 15 | 16 | 17 | license-mit 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 | HitUP 38 |
▲ Awesome! HitUP have Dark Theme now 🎉 🎉 🎉
39 |

40 | 41 | 42 |

43 | HitUP 44 |
▲ Trending Repositories This Week – Grid View
45 |

46 | 47 |

48 | HitUP 49 |
▲ Trending Repositories This Week – List View
50 |

51 | 52 |

53 | HitUP 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 |
48 | 49 | 50 |
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 | {m.username} 29 | 30 | { 36 | if (!this.state.readyForTooltipImgLoading) { return null } 37 | return <> 38 | {m.username} 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 | 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 |
17 |
18 |

19 | 20 | 21 | { this.props.repository.owner.login } 22 | 23 | / 24 | 25 | { this.props.repository.name } 26 | 27 |

28 | {this.props.repository.builtBy && this.props.repository.builtBy.length > 0 &&
Built by 29 | 30 |
} 31 |
32 |
33 |

{ this.props.repository.description || 'No description given.' }

34 |
35 |
36 | { 37 | this.props.repository.primaryLanguage && ( 38 | 39 | 42 |  {this.props.repository.primaryLanguage.name} 43 | 44 | ) 45 | } 46 | 50 | 51 | { this.props.repository.stargazers.totalCount } 52 | 53 | 57 | 58 | { this.props.repository.forkCount } 59 | 60 |
61 | 62 | 66 | { 69 | e.target.src = '/img/logo.svg'; 70 | } } 71 | alt={ this.props.repository.owner.login }/> 72 | 73 |
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 | 63 | {showDropdown &&
64 | {/*
Created in this period
65 | 69 | */} 73 |
74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 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 |
39 | 40 | { this.props.repository.name } 41 | 42 |
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 |
57 | 61 | 62 | { this.props.repository.stars } 63 | 64 | 68 | 69 | { this.props.repository.forks.toLocaleString() } 70 | 71 | 72 | {`${this.props.repository.currentPeriodStars}`} 73 | 74 | 75 | {`${this.props.repository.currentPeriodStars} stars ${trendingPeriodDefs[this.props.dateJump].heading}`} 76 | 77 |
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 |
19 |
20 |

21 | 22 | { this.props.repository.owner.login } / 23 | { this.props.repository.name } 24 | 25 |

26 | {this.props.repository.builtBy && this.props.repository.builtBy.length > 0 &&
Built by 27 | 28 |
} 29 |
30 |
31 |

{ this.props.repository.description || 'No description given.' }

32 |
33 |
34 | { 35 | this.props.repository.language && ( 36 | 37 | {this.props.repository.languageColor && 38 | 39 |  {this.props.repository.language} 40 | } 41 | 42 | ) 43 | } 44 | 48 | 49 | { this.props.repository.stars } 50 | 51 | 55 | 56 | { this.props.repository.forks.toLocaleString() } 57 | 58 | 59 | {`${this.props.repository.currentPeriodStars}`} 60 | 61 | 62 | {`${this.props.repository.currentPeriodStars} stars ${trendingPeriodDefs[this.props.dateJump].heading}`} 63 | 64 |
65 | 66 | 70 | { 73 | e.target.src = '/img/logo.svg'; 74 | } } 75 | alt={ this.props.repository.owner.login }/> 76 | 77 |
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 |
179 |
180 | 188 |
189 |
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 | 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 | 56 | 57 | 58 | {isRunningExtension && 59 | 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 | logo 60 | 61 |
62 |

HitUP

63 |

Find top 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 | Share HitUP on Twitter 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 |