Building GitHub Profile Analytics using React || PART 2

Written by dmitryrastorguev | Published 2018/02/07
Tech Story Tags: react | github | data-analysis | data-analytics | github-profile-analytics

TLDRvia the TL;DR App

This is a step by step article on how to build a basic analytics tool for GitHub profiles, using React. The emphasis was placed on achieving functionality quickly, therefore the end result does require further code refactoring and styling. The article is formed of two parts (Part 1, Part 2) and is based on the following GitHub repository. It is also hosted in this website. Please note that both the repository and the website will evolve beyond these two articles.

This articles continues from Part 1, where we built a basic page that is capable of taking a user input, using it to request data from GitHub’s API and then display the response on the web page once it has been received.

In this part we will add 3 different sections: basic information, list of most popular and starred repos and, finally, analyse most common languages of user’s own repos and generate keywords to starred repos.

Lets begin by running npm install --save moment react-moment which is necessary to adjust the date for one of the variables. Next we will need to create a new component called ProfileDetails.jsx with the following code:

import React from 'react';import Moment from 'react-moment';

const imgStye = {borderRadius: "50%",width: "250px",height: "250px"};

const ProfileDetails = (props) => {return (<div><div>{props.infoclean.avatar_url ?<img src={props.infoclean.avatar_url}alt="Profile"style={imgStye}/> : null }</div><div>{props.infoclean.name ? <div><p>Name:</p><p>{props.infoclean.name}</p></div> : null }</div><div>{props.infoclean.bio ? <div><p>Bio:</p><p>{props.infoclean.bio}</p></div> : null }</div><div>{props.infoclean.created_at ? <div><p>Joined:</p><p>{<Moment from={new Date()}>{props.infoclean.created_at}</Moment>}</p></div> : null }</div><div>{props.infoclean.blog ? <div><p>Blog:</p><p><a href={props.infoclean.blog.search("http") !== -1 ? props.infoclean.blog: "http://" + props.infoclean.blog } target="_blank">{props.infoclean.blog}</a></p></div> : null }</div><div>{props.infoclean.location ? <div><p>Location:</p><p>{props.infoclean.location}</p></div> : null }</div><div>{props.infoclean.company ? <div><p>Company:</p><p>{props.infoclean.company}</p></div> : null }</div><div>{props.infoclean.public_repos ? <div><p>Public Repos:</p><p>{props.infoclean.public_repos}</p></div> : null }</div><div>{props.infoclean.followers ? <div><p>Followers:</p><p>{props.infoclean.followers}</p></div> : null }</div><div>{props.infoclean.following ? <div><p>Following:</p><p>{props.infoclean.following}</p></div> : null }</div><div>{props.infoclean.html_url ? <div><p><a href={props.infoclean.html_url} target="_blank">View on GitHub</a></p></div> : null }</div><div>{props.infoclean.login ? <div>{ <img src={"http://ghchart.rshah.org/"+props.infoclean.login} alt="Github chart" />}<br/><a href="https://ghchart.rshah.org/" target="_blank">Source for GitHub Chart API</a></div> : null }</div></div>)

};

export default ProfileDetails;

Similarly, App.jsx, will need to be imported changed accordingly.

import React, { Component } from 'react';import axios from 'axios';

import Form from './components/Form.jsx';import ProfileDetails from './components/ProfileDetails.jsx';

class App extends Component {constructor() {super();this.state = {gitun: 'No username',infoclean : '',formData: {username: '',},

}this.handleUserFormSubmit = this.handleUserFormSubmit.bind(this);this.handleFormChange= this.handleFormChange.bind(this);}

handleUserFormSubmit(event) {event.preventDefault();axios.get('https://api.github.com/users/'+this.state.formData.username).then(response => this.setState({gitun: response.data.login,infoclean: response.data,})).catch((err) => { console.log(err); });};

handleFormChange(event) {const obj = this.state.formData;obj[event.target.name] = event.target.value;this.setState(obj);};

render() {return (<div className="App"><header className="App-header"><h1 className="App-title">GitHub Analytics</h1></header><p className="App-intro">Watch this space...</p><hr></hr><FormformData={this.state.formData}handleUserFormSubmit={this.handleUserFormSubmit}handleFormChange={this.handleFormChange}/><hr></hr>Profile Details:<ProfileDetails infoclean={this.state.infoclean}/>

</div>);}}

export default App;

These changes allow us to pull in together various data points of the profile. Such as Name, Bio, Location, number of repos, followers and some others. Overall, the section will look something like this.

Now we will begin working on the list of own and starred repositories. To do this we first create a new component called SortedList.jsx :

import React from 'react';import Moment from 'react-moment';

const SortedList = (props) => {if (props.repitems) {return (<ul>{props.repitems.map((repitem) =><li key={repitem.id}><div><div><a href={repitem.html_url} target="_blank">{repitem.name}</a> || Started <Moment from={new Date()}>{repitem.created_at}</Moment></div><div><i>{repitem.description}</i></div><div>Language: {repitem.language} || Watchers: {repitem.watchers_count} || Forks: {repitem.forks_count}</div></div></li>)}</ul>)} else { return null;}};

export default SortedList;

We then update App.jsx accordingly:

import React, { Component } from 'react';import axios from 'axios';

import Form from './components/Form.jsx';import SortedList from './components/SortedList.jsx';import ProfileDetails from './components/ProfileDetails.jsx';

class App extends Component {constructor() {super();this.state = {gitun: 'No username',infoclean : '',formData: {username: '',},repitems: null,staritems: null,

}this.handleUserFormSubmit = this.handleUserFormSubmit.bind(this);this.handleFormChange= this.handleFormChange.bind(this);}

handleUserFormSubmit(event) {event.preventDefault();axios.get('https://api.github.com/users/'+this.state.formData.username).then(response => this.setState({gitun: response.data.login,infoclean: response.data,})).catch((err) => { console.log(err); });

axios.get('https://api.github.com/users/'+this.state.formData.username+'/repos').then(response => this.setState({repitems : response.data.filter(({fork}) => fork === false).sort((b, a) => (a.watchers_count + a.forks_count) - (b.watchers_count + b.forks_count)).slice(0,10)})).catch((err) => { console.log(err); });

axios.get('https://api.github.com/users/'+this.state.formData.username+'/starred').then(response => this.setState({staritems : response.data.filter(({fork}) => fork === false).sort((b, a) => (a.watchers_count + a.forks_count) - (b.watchers_count + b.forks_count)).slice(0,10)})).catch((err) => { console.log(err); });};

handleFormChange(event) {const obj = this.state.formData;obj[event.target.name] = event.target.value;this.setState(obj);};

render() {return (<div className="App"><header className="App-header"><h1 className="App-title">GitHub Analytics</h1></header><p className="App-intro">Watch this space...</p><hr></hr><FormformData={this.state.formData}handleUserFormSubmit={this.handleUserFormSubmit}handleFormChange={this.handleFormChange}/><hr></hr>Profile Details:<ProfileDetails infoclean={this.state.infoclean}/><hr></hr>Own Repositories:<SortedList repitems={this.state.repitems}/><hr></hr>Starred Repositories:<SortedList repitems={this.state.staritems}/>

</div>);}}

export default App;

The page should now be able to show items for each of the repositories lists:

Finally, we can run some basic analysis on received information for repositaries. For example, for user’s own repositaries we will count the number of repositaries for each programming language. This would be an indication of the most preferred language for each user. Similarly, we will extract the description for starred repositaries and produce a list of keywords, indicating preferred topics. This topic modelling process will be done using Latent Dirichlet Allocation .

Let’s begin by running npm install lda --save, which will install the required package. In order to allow for building process to happen, we will need to move lda folder in node_modules to src folder. As a result, lda's new path becomes src/lda. This will also allow us to import lda into App.jsx.

Next we will need to create a new LanguageList.jsx component.

import React from 'react';

const LanguageList = (props) => {if (props.langslist) {return (<ul>{Object.entries(props.langslist).map(([key,value]) =><li key={key}>{key} - {value}</li>)}</ul>)} else { return null;}};

export default LanguageList;

Finally, we can update App.jsx as follows:

import React, { Component } from 'react';import axios from 'axios';

import Form from './components/Form.jsx';import SortedList from './components/SortedList.jsx';import ProfileDetails from './components/ProfileDetails.jsx';import LanguageList from './components/LanguageList.jsx';

import lda from './lda';

class App extends Component {constructor() {super();this.state = {gitun: 'No username',infoclean : '',info: '',formData: {username: '',},repitems: null,staritems: null,replanguagecount: {},keywords: null]

}this.handleUserFormSubmit = this.handleUserFormSubmit.bind(this);this.handleFormChange= this.handleFormChange.bind(this);}

handleUserFormSubmit(event) {event.preventDefault();axios.get('https://api.github.com/users/'+this.state.formData.username).then(response => this.setState({gitun: response.data.login,infoclean: response.data,info : JSON.stringify(response.data, undefined, 2)})).catch((err) => { console.log(err); });

axios.get('https://api.github.com/users/'+this.state.formData.username+'/repos').then(response => {

var itemsWithFalseForks = response.data.filter(item => item.fork === false)

var sortedItems = itemsWithFalseForks.sort((b,a) => {if((a.watchers_count + a.forks_count) < (b.forks_count + b.watchers_count)){return -1}else if ((a.watchers_count + a.forks_count) > (b.forks_count + b.watchers_count)){return 1}else {return 0}})

let dictrlc = Object.assign({}, this.state.replanguagecount);for (var i = 0; i < itemsWithFalseForks.length; i++) {dictrlc[itemsWithFalseForks[i]['language']] = -~ dictrlc[itemsWithFalseForks[i]['language']]}

this.setState({repitems: sortedItems.slice(0,10),replanguagecount: dictrlc,})

}).catch((err) => { console.log(err); })

axios.get('https://api.github.com/users/'+this.state.formData.username+'/starred').then(response => {

var itemsWithFalseForks = response.data.filter(item => item.fork === false)

var sortedItems = itemsWithFalseForks.sort((b,a) => {if((a.watchers_count + a.forks_count) < (b.forks_count + b.watchers_count)){return -1}else if ((a.watchers_count + a.forks_count) > (b.forks_count + b.watchers_count)){return 1}else {return 0}})

var documents = []for (var i = 0; i < response.data.length; i++) {var descr = response.data[i]['description']if (descr != null) {var newtext = descr.match(/[^.!?]+[.!?]+/g)if (newtext != null) {documents = documents.concat(newtext)}}}var result = lda(documents, 3, 3);var keywords = new Set()for (var k = 0; k < 3; k++) {for (var j = 0; j < 3; j++) {keywords = keywords.add(result[k][j]['term']);}}

this.setState({staritems: sortedItems.slice(0,10),keywords: Array.from(keywords).join(', ')})

}).catch((err) => { console.log(err); })

};

handleFormChange(event) {const obj = this.state.formData;obj[event.target.name] = event.target.value;this.setState(obj);};

render() {return (<div className="App"><header className="App-header"><h1 className="App-title">GitHub Analytics</h1></header><p className="App-intro">Watch this space...</p><hr></hr><FormformData={this.state.formData}handleUserFormSubmit={this.handleUserFormSubmit}handleFormChange={this.handleFormChange}/><hr></hr>Profile Details:<ProfileDetails infoclean={this.state.infoclean}/><hr></hr>Own Repositories:<SortedList repitems={this.state.repitems}/><hr></hr>Starred Repositories:<SortedList repitems={this.state.staritems}/><hr></hr>Own Repos Language Count:<LanguageList langslist={this.state.replanguagecount}/>Keywords: {this.state.keywords}</div>);}}

To ensure GitHub Chart is also visible, change ProfileDetails.jsx to the following:

import React from 'react';import Moment from 'react-moment';

const imgStye = {borderRadius: "50%",width: "250px",height: "250px"};

const ProfileDetails = (props) => {return (<div><div>{props.infoclean.avatar_url ?<img src={props.infoclean.avatar_url}alt="Profile"style={imgStye}/> : null }</div><div>{props.infoclean.name ? <div><p>Name:</p><p>{props.infoclean.name}</p></div> : null }</div><div>{props.infoclean.bio ? <div><p>Bio:</p><p>{props.infoclean.bio}</p></div> : null }</div><div>{props.infoclean.created_at ? <div><p>Joined:</p><p>{<Moment from={new Date()}>{props.infoclean.created_at}</Moment>}</p></div> : null }</div><div>{props.infoclean.blog ? <div><p>Blog:</p><p><a href={props.infoclean.blog.search("http") !== -1 ? props.infoclean.blog: "http://" + props.infoclean.blog } target="_blank">{props.infoclean.blog}</a></p></div> : null }</div><div>{props.infoclean.location ? <div><p>Location:</p><p>{props.infoclean.location}</p></div> : null }</div><div>{props.infoclean.company ? <div><p>Company:</p><p>{props.infoclean.company}</p></div> : null }</div><div>{props.infoclean.public_repos ? <div><p>Public Repos:</p><p>{props.infoclean.public_repos}</p></div> : null }</div><div>{props.infoclean.followers ? <div><p>Followers:</p><p>{props.infoclean.followers}</p></div> : null }</div><div>{props.infoclean.following ? <div><p>Following:</p><p>{props.infoclean.following}</p></div> : null }</div><div>{props.infoclean.html_url ? <div><p><a href={props.infoclean.html_url} target="_blank">View on GitHub</a></p></div> : null }</div><div>{props.infoclean.login ? <div>{ <img src={"http://ghchart.rshah.org/"+props.infoclean.login} alt="Github chart" />}<br/><a href="https://ghchart.rshah.org/" target="_blank">Source for GitHub Chart API</a></div> : null }</div></div>)

};

export default ProfileDetails;

As a result, this produces the following output for my GitHub profile.

Further opportunities for improvement in this project, include adding styling, code refactoring and testing.

Feel free to track the progress of this project on GitHub and on its website.


Published by HackerNoon on 2018/02/07