├── src ├── actions │ ├── index.js │ └── github.js ├── image │ ├── logo.png │ ├── WX20190912-172947.png │ └── WX20190912-173245.png ├── components │ ├── sections │ │ ├── Colors.js │ │ ├── ChartOptions.js │ │ ├── Repository.js │ │ ├── Release.js │ │ ├── Fork.js │ │ ├── PullRequests.js │ │ ├── Commit.js │ │ ├── Issues.js │ │ └── Star.js │ ├── DataTypes.js │ ├── App.js │ ├── DataUnit.js │ ├── GithubStatistics.js │ └── DataSection.js ├── reducers │ ├── index.js │ └── github.js ├── App.test.js ├── css │ ├── index.css │ ├── DataSection.css │ ├── App.css │ └── normalize.css ├── index.js ├── serviceWorker.js └── scripts │ └── GithubFetcher.js ├── .env ├── .vscode └── settings.json ├── public ├── robots.txt ├── github-sign-16.png ├── github-sign-24.png ├── github-sign-256.png ├── github-sign-32.png ├── github-sign-512.png ├── github-sign-64.png ├── manifest.json └── index.html ├── .gitignore ├── .eslintrc.json ├── package.json ├── README.md └── csv.js /src/actions/index.js: -------------------------------------------------------------------------------- 1 | export * from './github' -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_GITHUB_API_TOKEN="${YOUR_GITHUB_API_TOKEN}" -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": true 3 | } -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesoft-inc/github-statistics/HEAD/src/image/logo.png -------------------------------------------------------------------------------- /public/github-sign-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesoft-inc/github-statistics/HEAD/public/github-sign-16.png -------------------------------------------------------------------------------- /public/github-sign-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesoft-inc/github-statistics/HEAD/public/github-sign-24.png -------------------------------------------------------------------------------- /public/github-sign-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesoft-inc/github-statistics/HEAD/public/github-sign-256.png -------------------------------------------------------------------------------- /public/github-sign-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesoft-inc/github-statistics/HEAD/public/github-sign-32.png -------------------------------------------------------------------------------- /public/github-sign-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesoft-inc/github-statistics/HEAD/public/github-sign-512.png -------------------------------------------------------------------------------- /public/github-sign-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesoft-inc/github-statistics/HEAD/public/github-sign-64.png -------------------------------------------------------------------------------- /src/image/WX20190912-172947.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesoft-inc/github-statistics/HEAD/src/image/WX20190912-172947.png -------------------------------------------------------------------------------- /src/image/WX20190912-173245.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesoft-inc/github-statistics/HEAD/src/image/WX20190912-173245.png -------------------------------------------------------------------------------- /src/components/sections/Colors.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | '#A2B449', 3 | '#E89A41', 4 | '#9EABCD', 5 | '#56BABD', 6 | '#D79AB3', 7 | '#E6978A', 8 | '#5EBE85', 9 | ] -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | 3 | import github from './github' 4 | 5 | const reducers = combineReducers({ 6 | github: github, 7 | }) 8 | 9 | export default reducers -------------------------------------------------------------------------------- /src/actions/github.js: -------------------------------------------------------------------------------- 1 | export const updateState = (state, data) => ({ 2 | type: 'UPDATE_STATE', 3 | payload: { state, data } 4 | }) 5 | 6 | export const updateStatsField = (state, stats) => ({ 7 | type: 'UPDATE_STATS_FIELD', 8 | payload: { state, stats } 9 | }) -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/DataTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Keys are used for reference 3 | * Values are used for displaying and passing 4 | */ 5 | 6 | export default { 7 | REPO: 'Repository', 8 | STAR: 'Star', 9 | FORK: 'Fork', 10 | COMMIT: 'Commit', 11 | RELEASE: 'Release', 12 | ISSUES: 'Issues', 13 | PULLREQUESTS: 'Pull Requests', 14 | } -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import GithubStatistics from './GithubStatistics' 3 | import '../css/App.css' 4 | class App extends React.Component { 5 | render() { 6 | return ( 7 |
8 | 9 |
10 | ) 11 | } 12 | } 13 | 14 | 15 | 16 | export default App 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/components/sections/ChartOptions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Common highchart options shared by 3 | * most of the charts 4 | */ 5 | 6 | import COLORS from './Colors' 7 | 8 | export default { 9 | title: { 10 | text: undefined, 11 | }, 12 | // xAxis: { 13 | // type: 'datetime', 14 | // }, 15 | legend: { 16 | itemStyle: { 17 | color: 'rgba(0, 0, 0, 0.85)', 18 | fontWeight: '300' 19 | } 20 | }, 21 | colors: COLORS, 22 | tooltip: { 23 | shadow: false, 24 | split: true, 25 | }, 26 | credits: { 27 | enabled: false, 28 | }, 29 | } -------------------------------------------------------------------------------- /src/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | overflow: scroll; 9 | white-space: nowrap; 10 | background-color: #fff; 11 | 12 | } 13 | 14 | header { 15 | display: block; 16 | } 17 | 18 | code { 19 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 20 | monospace; 21 | display: block; 22 | } -------------------------------------------------------------------------------- /src/css/DataSection.css: -------------------------------------------------------------------------------- 1 | .section-header { 2 | margin: 16px 0 0 0; 3 | padding: 8px; 4 | } 5 | 6 | .section-title { 7 | display: inline; 8 | margin: 0 16px; 9 | color: rgba(0, 0, 0, 0.85); 10 | font-weight: 600; 11 | font-size: 24px; 12 | } 13 | 14 | .info-tag { 15 | display: inline; 16 | font-size: 18px; 17 | margin: auto 0; 18 | } 19 | 20 | .data-card { 21 | padding: 12px 12px; 22 | } 23 | 24 | .stats-card { 25 | display: inline-block; 26 | margin: 0 12px 0 0; 27 | padding: 6px 12px 12px 12px; 28 | } 29 | 30 | .ant-tag.repo-tag { 31 | border-width: 0; 32 | background: 0%; 33 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import { Provider } from 'react-redux' 5 | import { createStore } from 'redux' 6 | import reducers from './reducers' 7 | 8 | import './css/normalize.css' 9 | import './css/index.css' 10 | import App from './components/App' 11 | import * as serviceWorker from './serviceWorker' 12 | 13 | const store = createStore(reducers) 14 | ReactDOM.render( 15 | 16 | 17 | 18 | 19 | 20 | , 21 | document.getElementById('root') 22 | ) 23 | 24 | // If you want your app to work offline and load faster, you can change 25 | // unregister() to register() below. Note this comes with some pitfalls. 26 | // Learn more about service workers: https://bit.ly/CRA-PWA 27 | serviceWorker.unregister() 28 | -------------------------------------------------------------------------------- /src/reducers/github.js: -------------------------------------------------------------------------------- 1 | const INITIAL_STATE = { 2 | repoData: [], 3 | repoStats: {}, 4 | 5 | starData: [], 6 | starStats: {}, 7 | 8 | forkData: [], 9 | forkStats: {}, 10 | 11 | releaseData: [], 12 | releaseStats: {}, 13 | 14 | githubApiToken: btoa(process.env.REACT_APP_GITHUB_API_TOKEN), 15 | } 16 | 17 | const github = (state = INITIAL_STATE, action) => { 18 | const { type, payload } = action 19 | switch (type) { 20 | case 'UPDATE_STATE': 21 | return { 22 | ...state, 23 | ...{[payload.state]: payload.data} 24 | } 25 | case 'UPDATE_STATS_FIELD': 26 | return Object.assign({}, state, { 27 | [payload.state] : { 28 | ...state[payload.state], 29 | ...payload.stats 30 | } 31 | }) 32 | default: 33 | return state 34 | } 35 | } 36 | 37 | export default github -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "GitHub Stats", 3 | "name": "GitHub Statistics", 4 | "icons": [ 5 | { 6 | "src": "github-sign-16.png", 7 | "type": "image/png", 8 | "sizes": "16x16" 9 | }, 10 | { 11 | "src": "github-sign-24.png", 12 | "type": "image/png", 13 | "sizes": "24x24" 14 | }, 15 | { 16 | "src": "github-sign-32.png", 17 | "type": "image/png", 18 | "sizes": "32x32" 19 | }, 20 | { 21 | "src": "github-sign-64.png", 22 | "type": "image/png", 23 | "sizes": "64x64" 24 | }, 25 | { 26 | "src": "github-sign-256.png", 27 | "type": "image/png", 28 | "sizes": "256x256" 29 | }, 30 | { 31 | "src": "github-sign-512.png", 32 | "type": "image/png", 33 | "sizes": "512x512" 34 | } 35 | ], 36 | "start_url": ".", 37 | "display": "standalone", 38 | "theme_color": "#000000", 39 | "background_color": "#ffffff" 40 | } 41 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "es6": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended" 10 | ], 11 | "globals": { 12 | "Atomics": "readonly", 13 | "SharedArrayBuffer": "readonly" 14 | }, 15 | "parserOptions": { 16 | "ecmaFeatures": { 17 | "jsx": true 18 | }, 19 | "ecmaVersion": 2018, 20 | "sourceType": "module" 21 | }, 22 | "plugins": [ 23 | "react" 24 | ], 25 | "rules": { 26 | "indent": [ 27 | "error", 28 | 2, 29 | { 30 | "SwitchCase": 1 31 | } 32 | ], 33 | "comma-dangle": 0, // disallow trailing commas in object literals 34 | "semi": [ 35 | 1, 36 | "never" 37 | ], 38 | "react/jsx-filename-extension": [ 39 | 1, 40 | { 41 | "extensions": [ 42 | ".js", 43 | ".jsx" 44 | ] 45 | } 46 | ], 47 | "no-trailing-spaces": 1 // disallow trailing whitespace at the end of lines 48 | } 49 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-statistics", 3 | "homepage": "https://vesoft-inc.github.io/github-statistics", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@material-ui/core": "^4.11.4", 8 | "antd": "^3.26.20", 9 | "axios": "^0.26.1", 10 | "gh-pages": "^2.2.0", 11 | "graphql": "^14.7.0", 12 | "graphql-request": "^1.8.2", 13 | "highcharts": "^9.1.1", 14 | "highcharts-react-official": "^2.2.2", 15 | "lodash": "^4.17.21", 16 | "moment": "^2.29.1", 17 | "objects-to-csv": "^1.3.6", 18 | "react": "^16.14.0", 19 | "react-dom": "^16.14.0", 20 | "react-redux": "^7.2.4", 21 | "react-scripts": "3.1.1", 22 | "recharts": "^1.8.5", 23 | "redux": "^4.1.0" 24 | }, 25 | "scripts": { 26 | "predeploy": "npm run build", 27 | "deploy": "gh-pages -d build", 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject", 32 | "export": "node csv" 33 | }, 34 | "eslintConfig": { 35 | "extends": "react-app" 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.2%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | }, 49 | "devDependencies": { 50 | "eslint": "^6.8.0", 51 | "eslint-plugin-react": "^7.24.0", 52 | "redux-devtools": "^3.7.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | GitHub Statistics 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 |

3 | GitHub Statistics 4 |

5 | --- 6 | 7 | ![trynow](https://img.shields.io/badge/TRY-NOW-green?url=https://vesoft-inc.github.io/github-statistics/) 8 | ![GitHub stars](https://img.shields.io/github/stars/vesoft-inc/github-statistics?style=social) 9 | ![GitHub forks](https://img.shields.io/github/forks/vesoft-inc/github-statistics?style=social) 10 | ![GitHub issues](https://img.shields.io/github/issues/vesoft-inc/github-statistics) 11 | ![GitHub pull requests](https://img.shields.io/github/issues-pr/vesoft-inc/github-statistics) 12 | 13 | # Screenshots 14 | 15 | # Start 16 | 17 | ![Image of Yaktocat](/src/image/WX20190912-172947.png) 18 | 19 | # Commit 20 | 21 | ![Image of Yaktocat](/src/image/WX20190912-173245.png) 22 | 23 | # Features 24 | 25 | - [x] Repository overview 26 | - [x] Star history 27 | - [x] Fork history 28 | - [x] Commit history (recent year) 29 | - [x] Release assets (newest tag) 30 | 31 | # Contributions 32 | 33 | ![contributions](https://img.shields.io/badge/Contributions-are_welcomed-green)
34 | 35 | If you have any suggestions of comments on either current codes or features, do not heisitate to create issues/PRs to let us know. 36 | 37 | Feature requests are also warmly welcomed. 38 | 39 | # Development 40 | 41 | 1. clone repo. 42 | 43 | ```shell 44 | git clone https://github.com/vesoft-inc/github-statistics.git 45 | ``` 46 | 47 | 2. install npm modules. 48 | 49 | ```shell 50 | cd github-statistics 51 | npm install 52 | ``` 53 | 54 | 3. **MUST SET** `YOUR_GITHUB_API_TOKEN` in the [.env](./.env) file. Read [Creating a personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) to create token if you don't have one. 55 | 56 | 4. runs the app in the development mode, open [http://localhost:3000](http://localhost:3000) to view it in the browser. 57 | 58 | ```shell 59 | npm start 60 | ``` 61 | 62 | Build the app for production to the `build` folder.
63 | 64 | ```shell 65 | npm run build 66 | ``` 67 | 68 | --- 69 | 70 |

71 | # Made with ❤️ by vesoft-inc # 72 |

73 | 74 | --- 75 | -------------------------------------------------------------------------------- /src/css/App.css: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | 3 | .header { 4 | position: fixed; 5 | width: 100%; 6 | background-color: #FFFFFF; 7 | z-index: 10; 8 | padding: 12px 2%; 9 | box-shadow: 0 2px 8px #f0f1f2; 10 | /* border-bottom: 1px solid #999999 */ 11 | } 12 | 13 | .container { 14 | padding-top:60px; 15 | display: flex; 16 | flex-direction: row; 17 | margin: 0 2%; 18 | } 19 | 20 | .footer { 21 | width: 100%; 22 | height: auto; 23 | margin-top: 40px; 24 | background-color: rgba(28, 28, 30, 1); 25 | } 26 | 27 | .sider { 28 | } 29 | 30 | @media(max-width:600px){ 31 | .sider { 32 | display: none; 33 | } 34 | } 35 | 36 | .content { 37 | /* text-align: center; */ 38 | width: 100%; 39 | } 40 | 41 | /* Global reusable */ 42 | .flex-center { 43 | display: flex; 44 | place-content: center; 45 | align-items: center; 46 | } 47 | 48 | .flex-center-left { 49 | display: flex; 50 | justify-content: flex-start; 51 | align-content: center; 52 | align-items: center; 53 | } 54 | 55 | 56 | /* Specific on top-level */ 57 | .header-title { 58 | color: rgba(0, 0, 0, 0.85); 59 | font-weight: 600; 60 | font-size: 24px; 61 | } 62 | 63 | .header-title:hover { 64 | color: rgba(10, 132, 255, 1) 65 | } 66 | 67 | .header-section { 68 | margin: auto 12px; 69 | padding: 2px 0px; 70 | } 71 | 72 | .header-logo{ 73 | position: absolute; 74 | right: 0; 75 | top: 0; 76 | width: 180px; 77 | height: 65px; 78 | z-index: 99; 79 | } 80 | 81 | .header-input { 82 | margin-right: 12px; 83 | width: 200px; 84 | } 85 | 86 | /* OVERLOADS */ 87 | a:hover { 88 | color:rgba(0, 0, 0, 1); 89 | } 90 | 91 | .ant-anchor { 92 | font-size: 15px; 93 | padding: 20px 20px; 94 | } 95 | 96 | .ant-anchor-ink::before { 97 | position: relative; 98 | display: block; 99 | width: 0px; 100 | height: 100%; 101 | margin: 0 auto; 102 | content: ''; 103 | } 104 | 105 | .ant-anchor-ink-ball { 106 | width: 1px; 107 | height: 8px; 108 | border: 1px solid #222; 109 | border-radius: 8px; 110 | } 111 | 112 | .ant-anchor-link-title.ant-anchor-link-title-active { 113 | color: rgba(0, 0, 0, 1); 114 | } 115 | -------------------------------------------------------------------------------- /src/components/sections/Repository.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import moment from 'moment' 5 | 6 | import { Row, Statistic, Icon, Tag } from 'antd' 7 | 8 | import COLORS from './Colors' 9 | 10 | class Repository extends React.Component { 11 | 12 | _render = () => { 13 | const { stats, ready } = this.props 14 | 15 | return ( 16 | <> 17 | {Array.from(stats.entries()).map(( 18 | (pair, index) => { 19 | if (ready.get(pair[0])) { 20 | const { name, createdAt, primaryLanguage, pushedAt, watcherCount } = pair[1] 21 | const dateSinceCreated = Math.floor((Date.now() - new Date(createdAt).valueOf()) / (24*60*60*1000)) 22 | 23 | return ( 24 |
25 | 26 | 27 | {pair[0]} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | } value={watcherCount} /> 47 | 48 | 49 |
50 | ) 51 | } 52 | return false 53 | } 54 | ))} 55 | 56 | ) 57 | } 58 | 59 | render() { 60 | return ( 61 | <> 62 | {this._render()} 63 | 64 | ) 65 | } 66 | } 67 | 68 | Repository.propTypes = { 69 | id: PropTypes.string, 70 | repos: PropTypes.array, 71 | data: PropTypes.objectOf(Map), 72 | stats: PropTypes.objectOf(Map), 73 | ready: PropTypes.objectOf(Map), 74 | } 75 | 76 | 77 | export default Repository -------------------------------------------------------------------------------- /src/components/DataUnit.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import _ from 'lodash' 4 | 5 | import { Card, Progress, Button, Row, Col, Icon, Divider } from 'antd' 6 | 7 | class DataUnit extends React.Component { 8 | constructor(props) { 9 | super(props) 10 | 11 | this.state = { 12 | loading: false, 13 | progress: 0, 14 | ready: false, 15 | } 16 | 17 | } 18 | 19 | _renderSection = (title, iconType = 'question', iconColor = 'black', action) => ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | {title} 30 |
31 |
32 | 33 | 34 | 35 | {action} 36 | 37 | 38 |
39 | ) 40 | 41 | _renderGetButton = action => { 42 | const { loading, progress } = this.state 43 | 44 | const updateProgress = progress => this.setState({ progress }) 45 | const wrappedOnUpdate = param => { 46 | action.onUpdate(param) 47 | } 48 | const wrappedFinish = param => { 49 | this.setState({ loading: false, ready: true }) 50 | action.onFinish(param) 51 | } 52 | const scriptCall = _.partial(action.type, wrappedOnUpdate, wrappedFinish, updateProgress) 53 | const wrappedOnClick = () => { 54 | this.setState({ loading: true, progress: 0 }) 55 | scriptCall() 56 | } 57 | return ( 58 |
59 | 62 | 63 |
64 | ) 65 | } 66 | 67 | render() { 68 | const { ready } = this.state 69 | const { 70 | id, 71 | title, 72 | iconType, 73 | iconColor, 74 | children, 75 | action, 76 | } = this.props 77 | return ( 78 |
79 | {this._renderSection( 80 | title, iconType, iconColor, 81 | this._renderGetButton(action) 82 | )} 83 | {ready && children} 84 | 85 |
86 | ) 87 | } 88 | } 89 | 90 | DataUnit.propTypes = { 91 | id: PropTypes.string, 92 | children: PropTypes.any, 93 | title: PropTypes.string, 94 | iconColor: PropTypes.string, 95 | iconType: PropTypes.string, 96 | action: PropTypes.object, 97 | } 98 | 99 | export default DataUnit -------------------------------------------------------------------------------- /csv.js: -------------------------------------------------------------------------------- 1 | const ObjectToCsv = require('objects-to-csv'); 2 | const axios = require('axios'); 3 | 4 | const token = 'token ' // Enter your Github token 5 | 6 | const repos = [ 7 | { 8 | 'url': 'vesoft-inc/nebula', // repo github url 9 | 'name': 'nebula' 10 | }, 11 | { 12 | 'url': 'esoft-inc/github-statistics', 13 | 'name': 'github-statistics' 14 | }, 15 | ]; 16 | 17 | let csv_datas = []; 18 | (async () => { 19 | let data_item = {}; 20 | await Promise.all( 21 | repos.map(async _repo =>{ 22 | try { 23 | const { data } = await axios.get(`https://api.github.com/repos/${_repo.url}`, { 24 | headers: { 25 | "Authorization": token 26 | } 27 | }); 28 | // console.log("data",data) 29 | // commit 30 | const { headers:commit } = await axios.get(`https://api.github.com/repos/${data.full_name}/commits?per_page=1&page=1`, { 31 | headers: { 32 | "Authorization": token 33 | } 34 | }); 35 | 36 | // issue 37 | 38 | const { headers: issue } = await axios.get(`https://api.github.com/repos/${data.full_name}/issues?state=closed&per_page=1&page=1`, { 39 | headers: { 40 | "Authorization": token 41 | } 42 | }); 43 | 44 | // pull request count 45 | 46 | const {headers:pr_count_1} = await axios.get(`https://api.github.com/repos/${data.full_name}/pulls?state=open&per_page=1&page=1`, { 47 | headers: { 48 | "Authorization": token 49 | } 50 | }); 51 | const {headers:pr_count_2} = await axios.get(`https://api.github.com/repos/${data.full_name}/pulls?state=closed&per_page=1&page=1`, { 52 | headers: { 53 | "Authorization": token 54 | } 55 | }); 56 | 57 | data_item.repo_name = data.full_name.split("/")[0]; 58 | data_item.date = data.created_at.slice(0, 10); 59 | data_item.fork_count = data.forks_count; 60 | data_item.star_count = data.stargazers_count; 61 | data_item.issue_count = issue.link.split(',')[1].split("page=")[2].split(">")[0] + data.open_issues_count; 62 | data_item.pr_count = Number(pr_count_1.link.split(',')[1].split("page=")[2].split(">")[0] ) + Number(pr_count_2.link.split(',')[1].split("page=")[2].split(">")[0]); 63 | data_item.commit_count = commit.link.split('page=')[4].split(">")[0]; 64 | csv_datas = [...csv_datas, { ...data_item }]; 65 | } catch (error) { 66 | console.log('error:', error) 67 | } 68 | }) 69 | ) 70 | 71 | 72 | const csv = new ObjectToCsv(csv_datas); 73 | // save to file 74 | 75 | await csv.toDisk('./repo_data.csv'); 76 | 77 | // csv has exported 78 | 79 | console.log('csv has exported'); 80 | 81 | })(); -------------------------------------------------------------------------------- /src/components/sections/Release.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import moment from 'moment' 5 | 6 | import { Row, Statistic, Icon, Tag, Table } from 'antd' 7 | 8 | import COLORS from './Colors' 9 | 10 | class Release extends React.Component { 11 | 12 | _render = () => { 13 | const { stats, data, ready } = this.props 14 | 15 | return ( 16 | <> 17 | {Array.from(ready.entries()).map(( 18 | (pair, index) => { 19 | if (pair[1]) { // ready 20 | const { totalAssets, name, tagName, createdAt, totalDownloads } = stats.get(pair[0]) 21 | 22 | const dateSinceCreated = Math.floor((Date.now() - new Date(createdAt).valueOf()) / (24*60*60*1000)) 23 | const averageDownloadsPerDay = totalDownloads / dateSinceCreated 24 | 25 | return ( 26 |
27 | 28 | 29 | {pair[0]} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | } /> 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ) 54 | } 55 | return false 56 | } 57 | ))} 58 | 59 | ) 60 | } 61 | 62 | render() { 63 | return ( 64 | <> 65 | {this._render()} 66 | 67 | ) 68 | } 69 | } 70 | 71 | const columns = [ 72 | { 73 | title: 'Asset', 74 | dataIndex: 'name', 75 | key: 'name', 76 | }, 77 | { 78 | title: 'Content type', 79 | dataIndex: 'contentType', 80 | key: 'contentType', 81 | }, 82 | { 83 | title: 'Downloads', 84 | dataIndex: 'downloadCount', 85 | key: 'downloadCount', 86 | }, 87 | { 88 | title: 'Created at', 89 | dataIndex: 'createdAt', 90 | key: 'createdAt', 91 | render: time => moment(time).format("MMMM Do YYYY, h:mm:ss a") 92 | }, 93 | { 94 | title: 'Updated at', 95 | dataIndex: 'updatedAt', 96 | key: 'updatedAt', 97 | render: time => moment(time).format("MMMM Do YYYY, h:mm:ss a") 98 | }, 99 | 100 | ] 101 | 102 | 103 | Release.propTypes = { 104 | id: PropTypes.string, 105 | repos: PropTypes.array, 106 | data: PropTypes.objectOf(Map), 107 | stats: PropTypes.objectOf(Map), 108 | ready: PropTypes.objectOf(Map), 109 | } 110 | 111 | 112 | export default Release -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/components/sections/Fork.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { Row, Statistic, Icon, Tag } from 'antd' 5 | // import { LineChart, Line, CartesianGrid, XAxis, YAxis, ResponsiveContainer, Legend, Tooltip as ChartToolTip } from 'recharts' 6 | import Highcharts from 'highcharts' 7 | import HighchartsReact from 'highcharts-react-official' 8 | 9 | import COLORS from './Colors' 10 | import OPTIONS from './ChartOptions' 11 | 12 | 13 | class Fork extends React.Component { 14 | constructor(props) { 15 | super(props) 16 | this.state = { 17 | isReset: false, 18 | arr: [] 19 | } 20 | } 21 | static formatter = (repo, data) => { 22 | // fprk total data, index 0 23 | let total = { name: repo, data: [] } 24 | // fork daily increment data, index 1 25 | let increment = { name: repo, data: [] } 26 | 27 | let cumulativeCount = 0 28 | data.forEach((value, key) => { 29 | cumulativeCount += value 30 | total.data.push([key, cumulativeCount]) 31 | increment.data.push([key, value]) 32 | }) 33 | 34 | return [total, increment] 35 | } 36 | 37 | shouldComponentUpdate(nextProps) { 38 | return !nextProps.loading && !Array.from(nextProps.ready.values()).includes(false) 39 | } 40 | 41 | cloneMap(map) { 42 | let obj = Object.create(null) 43 | for (let [k, v] of map) { 44 | obj[k] = v 45 | } 46 | obj = JSON.parse(JSON.stringify(obj)) 47 | let tmpMap = new Map() 48 | for (let k of Object.keys(obj)) { 49 | tmpMap.set(k, obj[k]) 50 | } 51 | return tmpMap 52 | } 53 | 54 | componentWillReceiveProps(props) { 55 | this.setState({ 56 | arr: this.cloneMap(props.data) 57 | }) 58 | } 59 | 60 | resetData(min, max) { 61 | Array.from(this.state.arr.values()).map(dataArray => dataArray[0]).forEach((value, index) => { 62 | let initial = 0 63 | value.data.forEach((obj, index) => { 64 | if (min <= obj[0] && max >= obj[0]) { 65 | if (!initial) { 66 | initial = obj[1] 67 | value.data[index - 1] = 0 68 | } 69 | } 70 | if (obj) { 71 | obj[1] -= initial 72 | } 73 | }) 74 | }) 75 | this.setState({ 76 | isReset: true 77 | }) 78 | } 79 | 80 | _renderStatistics = () => { 81 | const { stats, ready } = this.props 82 | 83 | return ( 84 | <> 85 | {Array.from(stats.entries()).map(( 86 | (pair, index) => { 87 | if (ready.get(pair[0])) { 88 | const { total, maxIncrement, createdAt } = pair[1] 89 | const dateSinceCreated = Math.floor((Date.now() - new Date(createdAt).valueOf()) / (24*60*60*1000)) 90 | const averagePerDay = total / dateSinceCreated 91 | return ( 92 |
93 | 94 | 95 | {pair[0]} 96 | 97 | 98 | 99 | } /> 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
110 | ) 111 | } 112 | return false 113 | } 114 | ))} 115 | 116 | ) 117 | } 118 | 119 | _renderCharts = () => { 120 | const { data, ready } = this.props 121 | 122 | if (!Array.from(ready.values()).includes(true)) return 123 | 124 | return ( 125 | <> 126 | { 132 | if (!event.resetSelection) { 133 | var min = event.xAxis[0].min; 134 | var max = event.xAxis[0].max; 135 | this.resetData(min, max) 136 | } else { 137 | this.setState({ 138 | arr: this.cloneMap(data) 139 | }) 140 | } 141 | } 142 | }, 143 | zoomType: 'x', 144 | type: 'line' 145 | }, 146 | xAxis: { 147 | type: 'datetime', 148 | }, 149 | yAxis: { 150 | gridLineWidth: 0, 151 | title: { 152 | text: 'total forks', 153 | }, 154 | }, 155 | series: Array.from(this.state.arr.values()).map(dataArray => dataArray[0]), 156 | }} 157 | /> 158 | dataArray[1]), 175 | }} 176 | /> 177 | 178 | ) 179 | } 180 | 181 | render() { 182 | return ( 183 | <> 184 | {this._renderStatistics()} 185 | {this._renderCharts()} 186 | 187 | ) 188 | } 189 | } 190 | 191 | Fork.propTypes = { 192 | id: PropTypes.string, 193 | repos: PropTypes.array, 194 | data: PropTypes.objectOf(Map), 195 | stats: PropTypes.objectOf(Map), 196 | ready: PropTypes.objectOf(Map), 197 | loading: PropTypes.bool, 198 | } 199 | 200 | 201 | export default Fork -------------------------------------------------------------------------------- /src/components/sections/PullRequests.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { Row, Statistic, Icon, Tag } from 'antd' 5 | // import { LineChart, Line, CartesianGrid, XAxis, YAxis, ResponsiveContainer, Legend, Tooltip as ChartToolTip } from 'recharts' 6 | import Highcharts from 'highcharts' 7 | import HighchartsReact from 'highcharts-react-official' 8 | 9 | import COLORS from './Colors' 10 | import OPTIONS from './ChartOptions' 11 | 12 | 13 | class Fork extends React.Component { 14 | constructor(props) { 15 | super(props) 16 | this.state = { 17 | isReset: false, 18 | arr: [] 19 | } 20 | } 21 | static formatter = (repo, data) => { 22 | // fprk total data, index 0 23 | let total = { name: repo, data: [] } 24 | // fork daily increment data, index 1 25 | let increment = { name: repo, data: [] } 26 | 27 | let cumulativeCount = 0 28 | data.forEach((value, key) => { 29 | cumulativeCount += value 30 | total.data.push([key, cumulativeCount]) 31 | increment.data.push([key, value]) 32 | }) 33 | 34 | return [total, increment] 35 | } 36 | 37 | shouldComponentUpdate(nextProps) { 38 | return !nextProps.loading && !Array.from(nextProps.ready.values()).includes(false) 39 | } 40 | 41 | cloneMap(map) { 42 | let obj = Object.create(null) 43 | for (let [k, v] of map) { 44 | obj[k] = v 45 | } 46 | obj = JSON.parse(JSON.stringify(obj)) 47 | let tmpMap = new Map() 48 | for (let k of Object.keys(obj)) { 49 | tmpMap.set(k, obj[k]) 50 | } 51 | return tmpMap 52 | } 53 | 54 | componentWillReceiveProps(props) { 55 | this.setState({ 56 | arr: this.cloneMap(props.data) 57 | }) 58 | } 59 | 60 | resetData(min, max) { 61 | Array.from(this.state.arr.values()).map(dataArray => dataArray[0]).forEach((value, index) => { 62 | let initial = 0 63 | value.data.forEach((obj, index) => { 64 | if (min <= obj[0] && max >= obj[0]) { 65 | if (!initial) { 66 | initial = obj[1] 67 | value.data[index - 1] = 0 68 | } 69 | } 70 | if (obj) { 71 | obj[1] -= initial 72 | } 73 | }) 74 | }) 75 | this.setState({ 76 | isReset: true 77 | }) 78 | } 79 | 80 | _renderStatistics = () => { 81 | const { stats, ready } = this.props 82 | 83 | return ( 84 | <> 85 | {Array.from(stats.entries()).map(( 86 | (pair, index) => { 87 | if (ready.get(pair[0])) { 88 | const { total, maxIncrement, createdAt } = pair[1] 89 | const dateSinceCreated = Math.floor((Date.now() - new Date(createdAt).valueOf()) / (24*60*60*1000)) 90 | const averagePerDay = total / dateSinceCreated 91 | return ( 92 |
93 | 94 | 95 | {pair[0]} 96 | 97 | 98 | 99 | } /> 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
110 | ) 111 | } 112 | return false 113 | } 114 | ))} 115 | 116 | ) 117 | } 118 | 119 | _renderCharts = () => { 120 | const { data, ready } = this.props 121 | 122 | if (!Array.from(ready.values()).includes(true)) return 123 | 124 | return ( 125 | <> 126 | { 132 | if (!event.resetSelection) { 133 | var min = event.xAxis[0].min; 134 | var max = event.xAxis[0].max; 135 | this.resetData(min, max) 136 | } else { 137 | this.setState({ 138 | arr: this.cloneMap(data) 139 | }) 140 | } 141 | } 142 | }, 143 | zoomType: 'x', 144 | type: 'line' 145 | }, 146 | xAxis: { 147 | type: 'datetime', 148 | }, 149 | yAxis: { 150 | gridLineWidth: 0, 151 | title: { 152 | text: 'total pull requests', 153 | }, 154 | }, 155 | series: Array.from(this.state.arr.values()).map(dataArray => dataArray[0]), 156 | }} 157 | /> 158 | dataArray[1]), 175 | }} 176 | /> 177 | 178 | ) 179 | } 180 | 181 | render() { 182 | return ( 183 | <> 184 | {this._renderStatistics()} 185 | {this._renderCharts()} 186 | 187 | ) 188 | } 189 | } 190 | 191 | Fork.propTypes = { 192 | id: PropTypes.string, 193 | repos: PropTypes.array, 194 | data: PropTypes.objectOf(Map), 195 | stats: PropTypes.objectOf(Map), 196 | ready: PropTypes.objectOf(Map), 197 | loading: PropTypes.bool, 198 | } 199 | 200 | 201 | export default Fork -------------------------------------------------------------------------------- /src/components/sections/Commit.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { Row, Statistic, Icon, Tag } from 'antd' 5 | // import { LineChart, Line, CartesianGrid, XAxis, YAxis, ResponsiveContainer, Legend, Tooltip as ChartToolTip } from 'recharts' 6 | import Highcharts from 'highcharts' 7 | import HighchartsReact from 'highcharts-react-official' 8 | 9 | import COLORS from './Colors' 10 | import OPTIONS from './ChartOptions' 11 | 12 | 13 | class Commit extends React.Component { 14 | constructor(props) { 15 | super(props) 16 | this.state = { 17 | isReset: false, 18 | arr: [] 19 | } 20 | } 21 | static formatter = (repo, data) => { 22 | // commit total data, index 0 23 | let total = { name: repo, data: [] } 24 | // commit daily increment data, index 1 25 | let increment = { name: repo, data: [] } 26 | 27 | let cumulativeCount = 0 28 | 29 | // traversal backwards 30 | Array.from(data.entries()).slice().reverse().forEach( 31 | pair => { 32 | cumulativeCount += pair[1] 33 | total.data.push([pair[0], cumulativeCount]) 34 | increment.data.push([pair[0], pair[1]]) 35 | } 36 | ) 37 | 38 | return [total, increment] 39 | } 40 | 41 | shouldComponentUpdate(nextProps) { 42 | return !nextProps.loading && !Array.from(nextProps.ready.values()).includes(false) 43 | } 44 | 45 | cloneMap(map) { 46 | let obj = Object.create(null) 47 | for (let [k, v] of map) { 48 | obj[k] = v 49 | } 50 | obj = JSON.parse(JSON.stringify(obj)) 51 | let tmpMap = new Map() 52 | for (let k of Object.keys(obj)) { 53 | tmpMap.set(k, obj[k]) 54 | } 55 | return tmpMap 56 | } 57 | 58 | componentWillReceiveProps(props) { 59 | this.setState({ 60 | arr: this.cloneMap(props.data) 61 | }) 62 | } 63 | 64 | resetData(min, max) { 65 | Array.from(this.state.arr.values()).map(dataArray => dataArray[0]).forEach((value, index) => { 66 | let initial = 0 67 | value.data.forEach((obj, index) => { 68 | if (min <= obj[0] && max >= obj[0]) { 69 | if (!initial) { 70 | initial = obj[1] 71 | value.data[index - 1] = 0 72 | } 73 | } 74 | if (obj) { 75 | obj[1] -= initial 76 | } 77 | }) 78 | }) 79 | this.setState({ 80 | isReset: true 81 | }) 82 | } 83 | 84 | _renderStatistics = () => { 85 | const { stats, ready } = this.props 86 | 87 | return ( 88 | <> 89 | {Array.from(stats.entries()).map(( 90 | (pair, index) => { 91 | if (ready.get(pair[0])) { 92 | const { total, maxIncrement, createdAt } = pair[1] 93 | const dateSinceCreated = Math.floor((Date.now() - new Date(createdAt).valueOf()) / (24*60*60*1000)) 94 | const averagePerDay = total / dateSinceCreated 95 | return ( 96 |
97 | 98 | 99 | {pair[0]} 100 | 101 | 102 | 103 | } /> 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
114 | ) 115 | } 116 | return false 117 | } 118 | ))} 119 | 120 | ) 121 | } 122 | 123 | _renderCharts = () => { 124 | const { data, ready } = this.props 125 | 126 | if (!Array.from(ready.values()).includes(true)) return 127 | 128 | return ( 129 | <> 130 | { 136 | if (!event.resetSelection) { 137 | var min = event.xAxis[0].min; 138 | var max = event.xAxis[0].max; 139 | this.resetData(min, max) 140 | } else { 141 | this.setState({ 142 | arr: this.cloneMap(data) 143 | }) 144 | } 145 | } 146 | }, 147 | zoomType: 'x', 148 | type: 'line' 149 | }, 150 | xAxis: { 151 | type: 'datetime', 152 | }, 153 | yAxis: { 154 | gridLineWidth: 0, 155 | title: { 156 | text: 'total commits', 157 | }, 158 | }, 159 | series: Array.from(this.state.arr.values()).map(dataArray => dataArray[0]), 160 | }} 161 | /> 162 | dataArray[1]), 179 | }} 180 | /> 181 | 182 | ) 183 | } 184 | 185 | render() { 186 | return ( 187 | <> 188 | {this._renderStatistics()} 189 | {this._renderCharts()} 190 | 191 | ) 192 | } 193 | } 194 | 195 | Commit.propTypes = { 196 | id: PropTypes.string, 197 | repos: PropTypes.array, 198 | data: PropTypes.objectOf(Map), 199 | stats: PropTypes.objectOf(Map), 200 | ready: PropTypes.objectOf(Map), 201 | loading: PropTypes.bool, 202 | } 203 | 204 | 205 | export default Commit -------------------------------------------------------------------------------- /src/components/sections/Issues.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { Row, Statistic, Icon, Tag } from 'antd' 5 | // import { LineChart, Line, CartesianGrid, XAxis, YAxis, ResponsiveContainer, Legend, Tooltip as ChartToolTip } from 'recharts' 6 | import Highcharts from 'highcharts' 7 | import HighchartsReact from 'highcharts-react-official' 8 | 9 | import COLORS from './Colors' 10 | import OPTIONS from './ChartOptions' 11 | 12 | 13 | class Fork extends React.Component { 14 | constructor(props) { 15 | super(props) 16 | this.state = { 17 | isReset: false, 18 | arr: [] 19 | } 20 | } 21 | 22 | static formatter = (repo, data) => { 23 | // issues total data, index 0 24 | let total = { name: repo, data: [] } 25 | // issues daily increment data, index 1 26 | let increment = { name: repo, data: [] } 27 | let cumulativeCount = 0 28 | let arrayObj = Array.from(data); 29 | arrayObj.sort(function (a, b) { 30 | return a[0] - b[0]; 31 | }) 32 | arrayObj.forEach((value, key) => { 33 | cumulativeCount += value[1] 34 | total.data.push([value[0], cumulativeCount]) 35 | 36 | increment.data.push([value[0], value]) 37 | }) 38 | return [total, increment] 39 | } 40 | 41 | shouldComponentUpdate(nextProps) { 42 | return !nextProps.loading && !Array.from(nextProps.ready.values()).includes(false) 43 | } 44 | 45 | cloneMap(map) { 46 | let obj = Object.create(null) 47 | for (let [k, v] of map) { 48 | obj[k] = v 49 | } 50 | obj = JSON.parse(JSON.stringify(obj)) 51 | let tmpMap = new Map() 52 | for (let k of Object.keys(obj)) { 53 | tmpMap.set(k, obj[k]) 54 | } 55 | return tmpMap 56 | } 57 | 58 | componentWillReceiveProps(props) { 59 | this.setState({ 60 | arr: this.cloneMap(props.data) 61 | }) 62 | } 63 | 64 | resetData(min, max) { 65 | Array.from(this.state.arr.values()).map(dataArray => dataArray[0]).forEach((value, index) => { 66 | let initial = 0 67 | value.data.forEach((obj, index) => { 68 | if (min <= obj[0] && max >= obj[0]) { 69 | if (!initial) { 70 | initial = obj[1] 71 | value.data[index - 1] = 0 72 | } 73 | } 74 | if (obj) { 75 | obj[1] -= initial 76 | } 77 | }) 78 | }) 79 | this.setState({ 80 | isReset: true 81 | }) 82 | } 83 | 84 | _renderStatistics = () => { 85 | const { stats, ready } = this.props 86 | 87 | return ( 88 | <> 89 | {Array.from(stats.entries()).map(( 90 | (pair, index) => { 91 | if (ready.get(pair[0])) { 92 | const { total, maxIncrement, createdAt } = pair[1] 93 | const dateSinceCreated = Math.floor((Date.now() - new Date(createdAt).valueOf()) / (24*60*60*1000)) 94 | const averagePerDay = total / dateSinceCreated 95 | return ( 96 |
97 | 98 | 99 | {pair[0]} 100 | 101 | 102 | 103 | } /> 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
114 | ) 115 | } 116 | return false 117 | } 118 | ))} 119 | 120 | ) 121 | } 122 | 123 | _renderCharts = () => { 124 | const { data, ready } = this.props 125 | 126 | if (!Array.from(ready.values()).includes(true)) return 127 | 128 | return ( 129 | <> 130 | { 136 | if (!event.resetSelection) { 137 | var min = event.xAxis[0].min; 138 | var max = event.xAxis[0].max; 139 | this.resetData(min, max) 140 | } else { 141 | this.setState({ 142 | arr: this.cloneMap(data) 143 | }) 144 | } 145 | } 146 | }, 147 | zoomType: 'x', 148 | type: 'line' 149 | }, 150 | xAxis: { 151 | type: 'datetime', 152 | }, 153 | yAxis: { 154 | gridLineWidth: 0, 155 | title: { 156 | text: 'total issues', 157 | }, 158 | }, 159 | series: Array.from(this.state.arr.values()).map(dataArray => dataArray[0]), 160 | }} 161 | /> 162 | dataArray[1]), 179 | }} 180 | /> 181 | 182 | ) 183 | } 184 | 185 | render() { 186 | return ( 187 | <> 188 | {this._renderStatistics()} 189 | {this._renderCharts()} 190 | 191 | ) 192 | } 193 | } 194 | 195 | Fork.propTypes = { 196 | id: PropTypes.string, 197 | repos: PropTypes.array, 198 | data: PropTypes.objectOf(Map), 199 | stats: PropTypes.objectOf(Map), 200 | ready: PropTypes.objectOf(Map), 201 | loading: PropTypes.bool, 202 | } 203 | 204 | 205 | export default Fork -------------------------------------------------------------------------------- /src/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { /* 1 */ 178 | overflow: visible; 179 | } 180 | 181 | /** 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 183 | * 1. Remove the inheritance of text transform in Firefox. 184 | */ 185 | 186 | button, 187 | select { /* 1 */ 188 | text-transform: none; 189 | } 190 | 191 | /** 192 | * Correct the inability to style clickable types in iOS and Safari. 193 | */ 194 | 195 | button, 196 | [type="button"], 197 | [type="reset"], 198 | [type="submit"] { 199 | -webkit-appearance: button; 200 | } 201 | 202 | /** 203 | * Remove the inner border and padding in Firefox. 204 | */ 205 | 206 | button::-moz-focus-inner, 207 | [type="button"]::-moz-focus-inner, 208 | [type="reset"]::-moz-focus-inner, 209 | [type="submit"]::-moz-focus-inner { 210 | border-style: none; 211 | padding: 0; 212 | } 213 | 214 | /** 215 | * Restore the focus styles unset by the previous rule. 216 | */ 217 | 218 | button:-moz-focusring, 219 | [type="button"]:-moz-focusring, 220 | [type="reset"]:-moz-focusring, 221 | [type="submit"]:-moz-focusring { 222 | outline: 1px dotted ButtonText; 223 | } 224 | 225 | /** 226 | * Correct the padding in Firefox. 227 | */ 228 | 229 | fieldset { 230 | padding: 0.35em 0.75em 0.625em; 231 | } 232 | 233 | /** 234 | * 1. Correct the text wrapping in Edge and IE. 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. 236 | * 3. Remove the padding so developers are not caught out when they zero out 237 | * `fieldset` elements in all browsers. 238 | */ 239 | 240 | legend { 241 | box-sizing: border-box; /* 1 */ 242 | color: inherit; /* 2 */ 243 | display: table; /* 1 */ 244 | max-width: 100%; /* 1 */ 245 | padding: 0; /* 3 */ 246 | white-space: normal; /* 1 */ 247 | } 248 | 249 | /** 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 251 | */ 252 | 253 | progress { 254 | vertical-align: baseline; 255 | } 256 | 257 | /** 258 | * Remove the default vertical scrollbar in IE 10+. 259 | */ 260 | 261 | textarea { 262 | overflow: auto; 263 | } 264 | 265 | /** 266 | * 1. Add the correct box sizing in IE 10. 267 | * 2. Remove the padding in IE 10. 268 | */ 269 | 270 | [type="checkbox"], 271 | [type="radio"] { 272 | box-sizing: border-box; /* 1 */ 273 | padding: 0; /* 2 */ 274 | } 275 | 276 | /** 277 | * Correct the cursor style of increment and decrement buttons in Chrome. 278 | */ 279 | 280 | [type="number"]::-webkit-inner-spin-button, 281 | [type="number"]::-webkit-outer-spin-button { 282 | height: auto; 283 | } 284 | 285 | /** 286 | * 1. Correct the odd appearance in Chrome and Safari. 287 | * 2. Correct the outline style in Safari. 288 | */ 289 | 290 | [type="search"] { 291 | -webkit-appearance: textfield; /* 1 */ 292 | outline-offset: -2px; /* 2 */ 293 | } 294 | 295 | /** 296 | * Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | [type="search"]::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /** 304 | * 1. Correct the inability to style clickable types in iOS and Safari. 305 | * 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; /* 1 */ 310 | font: inherit; /* 2 */ 311 | } 312 | 313 | /* Interactive 314 | ========================================================================== */ 315 | 316 | /* 317 | * Add the correct display in Edge, IE 10+, and Firefox. 318 | */ 319 | 320 | details { 321 | display: block; 322 | } 323 | 324 | /* 325 | * Add the correct display in all browsers. 326 | */ 327 | 328 | summary { 329 | display: list-item; 330 | } 331 | 332 | /* Misc 333 | ========================================================================== */ 334 | 335 | /** 336 | * Add the correct display in IE 10+. 337 | */ 338 | 339 | template { 340 | display: none; 341 | } 342 | 343 | /** 344 | * Add the correct display in IE 10. 345 | */ 346 | 347 | [hidden] { 348 | display: none; 349 | } -------------------------------------------------------------------------------- /src/components/sections/Star.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { Row, Statistic, Icon, Tag } from 'antd' 5 | // import { LineChart, Line, CartesianGrid, XAxis, YAxis, ResponsiveContainer, Legend, Tooltip as ChartToolTip } from 'recharts' 6 | import Highcharts from 'highcharts' 7 | import HighchartsReact from 'highcharts-react-official' 8 | 9 | import COLORS from './Colors' 10 | import OPTIONS from './ChartOptions' 11 | 12 | 13 | class Star extends React.Component { 14 | constructor(props) { 15 | super(props) 16 | this.state = { 17 | isReset: false, 18 | arr: [] 19 | } 20 | } 21 | static formatter = (repo, data) => { 22 | // star total data, index 0 23 | let total = { name: repo, data: [] } 24 | // star daily increment data, index 1 25 | let increment = { name: repo, data: [] } 26 | 27 | let cumulativeCount = 0 28 | data.forEach((value, key) => { 29 | cumulativeCount += value 30 | total.data.push([key, cumulativeCount]) 31 | increment.data.push([key, value]) 32 | }) 33 | 34 | return [total, increment] 35 | } 36 | 37 | shouldComponentUpdate(nextProps) { 38 | return !nextProps.loading && !Array.from(nextProps.ready.values()).includes(false) 39 | } 40 | cloneMap(map) { 41 | let obj = Object.create(null); 42 | for (let [k, v] of map) { 43 | obj[k] = v; 44 | } 45 | obj = JSON.parse(JSON.stringify(obj)); 46 | let tmpMap = new Map(); 47 | for (let k of Object.keys(obj)) { 48 | tmpMap.set(k, obj[k]); 49 | } 50 | return tmpMap; 51 | } 52 | componentWillReceiveProps(props) { 53 | this.setState({ 54 | arr: this.cloneMap(props.data) 55 | }) 56 | } 57 | 58 | resetData(min, max) { 59 | Array.from(this.state.arr.values()).map(dataArray => dataArray[0]).forEach((value, index) => { 60 | let initial = 0 61 | value.data.forEach((obj, index) => { 62 | if (min <= obj[0] && max >= obj[0]) { 63 | if (!initial) { 64 | initial = obj[1] 65 | value.data[index-1] = 0 66 | } 67 | } 68 | if (obj) { 69 | obj[1] -= initial 70 | } 71 | }) 72 | }) 73 | this.setState({ 74 | isReset: true 75 | }) 76 | } 77 | _renderStatistics = () => { 78 | const { stats, ready } = this.props 79 | 80 | return ( 81 | <> 82 | {Array.from(stats.entries()).map(( 83 | (pair, index) => { 84 | if (ready.get(pair[0])) { 85 | const { total, maxIncrement, createdAt } = pair[1] 86 | const dateSinceCreated = Math.floor((Date.now() - new Date(createdAt).valueOf()) / (24 * 60 * 60 * 1000)) 87 | const averagePerDay = total / dateSinceCreated 88 | return ( 89 |
90 | 91 | 92 | {pair[0]} 93 | 94 | 95 | 96 | } /> 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 |
107 | ) 108 | } 109 | return false 110 | } 111 | ))} 112 | 113 | ) 114 | } 115 | 116 | _renderCharts = () => { 117 | const { data, ready } = this.props 118 | if (!Array.from(ready.values()).includes(true)) return 119 | return ( 120 | <> 121 | { 128 | if (!event.resetSelection) { 129 | var min = event.xAxis[0].min; 130 | var max = event.xAxis[0].max; 131 | this.resetData(min, max) 132 | } else { 133 | this.setState({ 134 | arr: this.cloneMap(data) 135 | }) 136 | } 137 | } 138 | }, 139 | zoomType: 'x', 140 | type: 'line' 141 | }, 142 | xAxis: { 143 | type: 'datetime', 144 | }, 145 | yAxis: { 146 | gridLineWidth: 0, 147 | title: { 148 | text: 'total stars', 149 | }, 150 | }, 151 | series: Array.from(this.state.arr.values()).map(dataArray => dataArray[0]), 152 | }} 153 | /> 154 | dataArray[1]), 172 | }} 173 | /> 174 | 175 | ) 176 | } 177 | 178 | // _renderLines = (dataIndex) => { 179 | // const { data, ready } = this.props 180 | // const dataReady = Array.from(ready.values()) 181 | // console.log("Lines are rendered") 182 | // return Array.from(data.values()).map((dataArray, index) => ( 183 | // dataReady[index] 184 | // ? 185 | // 194 | // : 195 | // <> 196 | // )) 197 | // } 198 | 199 | // _renderCharts = () => { 200 | // const { ready } = this.props 201 | 202 | // if (!Array.from(ready.values()).includes(true)) return 203 | 204 | // return ( 205 | // <> 206 | // 207 | //
208 | // 209 | // 210 | // 211 | // 212 | // new Date(ms).toISOString().slice(0,10)} 219 | // /> 220 | // 221 | // new Date(ms).toISOString().slice(0,10)}/> 222 | // {this._renderLines(0)} 223 | // 224 | // 225 | //
226 | //
227 | // 228 | // 229 | // 230 | // 231 | // new Date(ms).toISOString().slice(0,10)} 238 | // /> 239 | // 240 | // new Date(ms).toISOString().slice(0,10)}/> 241 | // {this._renderLines(1)} 242 | // 243 | // 244 | //
245 | //
246 | // 247 | // ) 248 | // } 249 | 250 | render() { 251 | return ( 252 | <> 253 | {this._renderStatistics()} 254 | {this._renderCharts()} 255 | 256 | ) 257 | } 258 | } 259 | 260 | Star.propTypes = { 261 | id: PropTypes.string, 262 | repos: PropTypes.array, 263 | data: PropTypes.objectOf(Map), 264 | stats: PropTypes.objectOf(Map), 265 | ready: PropTypes.objectOf(Map), 266 | loading: PropTypes.bool, 267 | } 268 | 269 | 270 | export default Star -------------------------------------------------------------------------------- /src/components/GithubStatistics.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import PropTypes from 'prop-types' 4 | import _ from 'lodash' 5 | 6 | import TYPES from './DataTypes' 7 | import COLORS from './sections/Colors' 8 | 9 | import { Row, Col, Anchor, Button, Tag, Tooltip, message, Select } from 'antd' 10 | 11 | import DataSection from './DataSection' 12 | 13 | import GithubFetcher from '../scripts/GithubFetcher' 14 | 15 | import { updateState } from '../actions' 16 | import logo from '../image/logo.png' 17 | // const CENTER_FLEX = { display: 'flex', placeContent: 'center' } 18 | // const CENTER_LEFT_FLEX = { display: 'flex', justifyContent: 'flex-start', alignContent: 'center'} 19 | 20 | // message.config({ 21 | // top: 60, 22 | // duration: 2, 23 | // maxCount: 5, 24 | // }) 25 | /* eslint-disable-next-line */ 26 | const GITHUB_API_TOKEN = process.env.REACT_APP_GITHUB_API_TOKEN 27 | 28 | class GithubStatistics extends React.Component { 29 | constructor(props) { 30 | super(props) 31 | 32 | this.state = { 33 | repos:[], 34 | input: undefined, 35 | suggestions: [], 36 | testingRepo: false, 37 | deleteRepo: '', 38 | } 39 | 40 | 41 | /* eslint-disable-next-line */ 42 | var t = btoa(GITHUB_API_TOKEN) 43 | /* eslint-disable-next-line */ 44 | this.fetcher = new GithubFetcher(t) 45 | this.props.updateState("githubApiToken", t) 46 | 47 | this.search = _.debounce( 48 | this.fetcher.searchRepository, 49 | 300, 50 | { leading: false, trailing: true } 51 | ).bind(this) 52 | } 53 | 54 | componentDidMount() { 55 | try { 56 | if (JSON.parse(localStorage.getItem("repos"))) { 57 | this.setState({ 58 | repos: JSON.parse(localStorage.getItem("repos")), 59 | deleteRepo: '', 60 | }) 61 | } 62 | } catch (error) { 63 | console.log(error) 64 | } 65 | } 66 | 67 | deleteRepo = index => { 68 | const { repos } = this.state 69 | const deleteRepo = repos[index] 70 | repos.splice(index, 1) 71 | this.setState({ 72 | repos: [...repos], 73 | deleteRepo: deleteRepo, 74 | }, () => { 75 | localStorage.setItem("repos", JSON.stringify([...repos])) 76 | }) 77 | } 78 | 79 | addRepo = repo => { 80 | const { repos } = this.state 81 | if (repos.includes(repo)) { 82 | message.error(`${repo} is already added`) 83 | }else { 84 | this.setState({ 85 | repos: [ ...repos, repo], 86 | deleteRepo: '', 87 | }, () => { 88 | localStorage.setItem("repos", JSON.stringify([...repos, repo])) 89 | }) 90 | } 91 | } 92 | 93 | // _handleAdding = repo => { 94 | // const slashIndex = repo.indexOf('/') 95 | // const owner = repo.slice(0, slashIndex) 96 | // const name = repo.slice(slashIndex + 1) 97 | 98 | // this.setState({ testingRepo: true }) 99 | // this.fetcher.testRepository(owner, name, 100 | // result => { 101 | // this.setState({ testingRepo: false }) 102 | // if (result) { 103 | // this.addRepo(repo) 104 | // message.success(repo + ' added') 105 | // } else { 106 | // message.error('Repository not found') 107 | // } 108 | // } 109 | // ) 110 | // } 111 | 112 | _renderTags = () => { 113 | const { repos } = this.state 114 | 115 | return ( 116 | repos.map((repo, index) => ( 117 | this.deleteRepo(index)}> 118 | {repo} 119 | 120 | )) 121 | ) 122 | } 123 | 124 | _renderHeaderInput = () => { 125 | const { repos, input, testingRepo, suggestions } = this.state 126 | 127 | // const format = /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}\/{1}[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i 128 | let hintMessage = '' 129 | 130 | // Conditions 131 | const inputEmpty = input === undefined 132 | // const formatIncorrect = !format.test(input) 133 | const repoExisted = repos.includes(input) 134 | 135 | if (repoExisted) hintMessage = 'Repository already added' 136 | // if (formatIncorrect) hintMessage = 'Input incorrectly formatted' 137 | if (inputEmpty) hintMessage = 'Empty' 138 | 139 | const disabled = inputEmpty || repoExisted 140 | 141 | return ( 142 | 143 | {/* } 146 | placeholder="owner/name" 147 | value={input} 148 | onChange={e => { 149 | this.setState({ input: e.target.value }) 150 | this.fetcher.searchRepository(e.target.value, suggestions => this.setState({ suggestions })) 151 | }} 152 | onPressEnter={() => !disabled && this._handleAdding(input)} 153 | disabled={testingRepo} 154 | allowClear 155 | /> */} 156 | 175 | 178 |
199 | 200 | GitHub Stats 201 | 202 | 203 |