diff --git a/routes.js b/routes.js index 3ea20c9..f4fbb56 100644 --- a/routes.js +++ b/routes.js @@ -1,12 +1,11 @@ import React from 'react'; import { Route } from 'react-router'; import HomeView from './views/RootView'; -import FishView from './views/FishView'; -import 'bootstrap/dist/css/bootstrap.min.css'; +var styles = require('./stylesheet.css'); +var bootstrap = require('./node_modules/bootstrap/dist/css/bootstrap.css'); const routes = ( - - + ); diff --git a/styles.scss b/styles.scss new file mode 100644 index 0000000..dae4800 --- /dev/null +++ b/styles.scss @@ -0,0 +1,147 @@ +$min-width :1100px; +$super-light-grey:#f5f5f5; +$light-grey :#e5e5e5; + +@mixin flex-centered() { + display : flex; + justify-content : center; +} + + +body { + display: flex; + justify-content: center; +} + + +.search-container, .title-container { + @include flex-centered; + min-width: $min-width; + #search-box { + width: 300px; + } +} + +.search-results-container { + position: absolute; + left: 400px; + top: 37px; + z-index: 1; + padding: 0; +} + +.results-display-container { + @include flex-centered; + flex-direction: column; + background-color: $super-light-grey; + border: 1px solid $light-grey; + padding: 30px; + height: 172px; + min-width: $min-width; + .search-container { + position: relative; + } +} + +.person-entry-view-holder { + @include flex-centered; + background-color: white; + .person-entry-view { + display: flex; + justify-content: space-between; + padding: 5px; + border: 1px solid $light-grey; + width: 300px; + .person-entry-view-image, .person-entry-view-spacer { + height: 50px; + width: 50px; + } + } +} + +.person-entry-view-holder:hover { + cursor: pointer; + background-color: $super-light-grey; +} + +.person-entry-view-no-results { + @include flex-centered; +} + + +/*profile partial css below*/ +.profile-container { + min-width: $min-width; +} + +.profile-left-section, .profile-center-section, .profile-right-section { + padding: 20px; +} + +/*profile left section*/ +.profile-left-container { + display: flex; + justify-content: flex-start; + flex-direction: column; + border: 1px solid $light-grey; + border-radius: 0.7em; + padding: 25px 20px; + height: 450px; +} + +.profile-left-section-picture { + height: 150px; + width: 150px; + align-self: center; + margin: 20px; +} + +/*profile center section*/ +.followers-list-container { + max-height: 400px; + overflow: scroll; +} + +#follower-entry-container { + padding: 0; +} + +.followers-list-entry { + display: flex; + justify-content: space-between; + padding: 8px 10px; + h6 { + overflow: scroll; + } +} + +.followers-list-entry:hover { + cursor: pointer; + background-color: $super-light-grey; +} + +.followers-list-image { + height: 40px; + width: 40px; +} + +/*profile right section*/ +.repo-container { + max-height: 400px; + overflow: scroll; +} + +.repo-entry { + div { + display : flex; + justify-content: space-between; + span { + border-radius: 0.7em; + height: 24px; + } + h6 { + overflow: scroll; + } + } +} + diff --git a/stylesheet.css b/stylesheet.css new file mode 100644 index 0000000..0c84658 --- /dev/null +++ b/stylesheet.css @@ -0,0 +1,113 @@ +body { + display: flex; + justify-content: center; } + +.search-container, .title-container { + display: flex; + justify-content: center; + min-width: 1100px; } + .search-container #search-box, .title-container #search-box { + width: 300px; } + +.search-results-container { + position: absolute; + left: 400px; + top: 37px; + z-index: 1; + padding: 0; } + +.results-display-container { + display: flex; + justify-content: center; + flex-direction: column; + background-color: #f5f5f5; + border: 1px solid #e5e5e5; + padding: 30px; + height: 172px; + min-width: 1100px; } + .results-display-container .search-container { + position: relative; } + +.person-entry-view-holder { + display: flex; + justify-content: center; + background-color: white; } + .person-entry-view-holder .person-entry-view { + display: flex; + justify-content: space-between; + padding: 5px; + border: 1px solid #e5e5e5; + width: 300px; } + .person-entry-view-holder .person-entry-view .person-entry-view-image, .person-entry-view-holder .person-entry-view .person-entry-view-spacer { + height: 50px; + width: 50px; } + +.person-entry-view-holder:hover { + cursor: pointer; + background-color: #f5f5f5; } + +.person-entry-view-no-results { + display: flex; + justify-content: center; } + +/*profile partial css below*/ +.profile-container { + min-width: 1100px; } + +.profile-left-section, .profile-center-section, .profile-right-section { + padding: 20px; } + +/*profile left section*/ +.profile-left-container { + display: flex; + justify-content: flex-start; + flex-direction: column; + border: 1px solid #e5e5e5; + border-radius: 0.7em; + padding: 25px 20px; + height: 450px; } + +.profile-left-section-picture { + height: 150px; + width: 150px; + align-self: center; + margin: 20px; } + +/*profile center section*/ +.followers-list-container { + max-height: 400px; + overflow: scroll; } + +#follower-entry-container { + padding: 0; } + +.followers-list-entry { + display: flex; + justify-content: space-between; + padding: 8px 10px; } + .followers-list-entry h6 { + overflow: scroll; } + +.followers-list-entry:hover { + cursor: pointer; + background-color: #f5f5f5; } + +.followers-list-image { + height: 40px; + width: 40px; } + +/*profile right section*/ +.repo-container { + max-height: 400px; + overflow: scroll; } + +.repo-entry div { + display: flex; + justify-content: space-between; } + .repo-entry div span { + border-radius: 0.7em; + height: 24px; } + .repo-entry div h6 { + overflow: scroll; } + +/*# sourceMappingURL=stylesheet.css.map */ diff --git a/utils/utils.js b/utils/utils.js new file mode 100644 index 0000000..db33e9f --- /dev/null +++ b/utils/utils.js @@ -0,0 +1,96 @@ +export function debounce (func, wait, immediate) { + let timeout; + return function() { + let context = this, args = arguments; + let later = () => { + timeout = null; + if (!immediate) { + func.apply(context, args); + } + }; + const callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + func.apply(context, args); + } + }; +}; + +export function fetchData (query, context) { + const request_url = 'https://api.github.com/search/users?q=' + query; + fetch(request_url) + .then((response) => { + return response.json(); + }) + .then((responseData) => { + context.setState({ + searchResults : responseData.items, + dirtySearch : true + }); + }) + .catch((err) => { + throw new Error(err); + }) +}; + +export function fetchUserData (query, context) { + const request_url = 'https://api.github.com/users/' + query; + fetch(request_url) + .then((response) => { + return response.json(); + }) + .then((responseData) => { + context.setState({ + currentFocus : responseData + }); + fetchUserFollowersRepos(context.state.currentFocus.repos_url, context, 'repos'); + fetchUserFollowersRepos(context.state.currentFocus.followers_url, context, 'followers'); + }) + .catch((err) => { + throw new Error(err); + }) +}; + +const fetchUserFollowersRepos = (query, context, update) => { + fetch(query) + .then((response) => { + return response.json(); + }) + .then((responseData) => { + if (update === 'repos') { + context.setState({ + currentFocusRepos : responseData + }); + } if (update === 'followers') { + context.setState({ + currentFocusFollowers : responseData + }); + } + }) + .catch((err) => { + throw new Error(err); + }) +}; + +export function stringifyDate (stringDate) { + const months = { + '01' : 'Jan', + '02' : 'Feb', + '03' : 'Mar', + '04' : 'Apr', + '05' : 'May', + '06' : 'Jun', + '07' : 'July', + '08' : 'Aug', + '09' : 'Sept', + '10' : 'Oct', + '11' : 'Nov', + '12' : 'Dec' + } + + let formattedDate = stringDate.substr(0, 10).split('-'); + formattedDate = months[formattedDate[1]]+' '+formattedDate[2]+' '+formattedDate[0]; + + return formattedDate; +}; \ No newline at end of file diff --git a/views/FishView/index.js b/views/FishView/index.js deleted file mode 100644 index 44fcbde..0000000 --- a/views/FishView/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -export default class FishView extends React.Component { - render () { - return ( -
-

No Fishes Here

-
- ); - } -} diff --git a/views/RootView/index.js b/views/RootView/index.js index 554ebd9..66147e0 100644 --- a/views/RootView/index.js +++ b/views/RootView/index.js @@ -1,16 +1,89 @@ import React from 'react'; +import {debounce, fetchData, fetchUserData} from '../../utils/utils.js'; +import SearchResultView from './SearchResultView.js'; +import ProfileView from '../profileView/index.js'; export default class RootView extends React.Component { + static propTypes = { children: React.PropTypes.any } + constructor (props) { + super (props); + this.state = { + searchResults : [], + currentFocus : null, + currentFocusRepos: [], + currentFocusFollowers: [], + dirtySearch: false + }; + } + + componentDidMount () { + this.debouncedGithubSearch = debounce ((query) => { + query = query.trim(); + if (query.length >= 3) { + fetchData(query, this); + } + if (query.length === 0) { + this.setState({ + searchResults: [], + dirtySearch : false + }); + } + }, 400); + } + + handleChange (event) { + this.debouncedGithubSearch(event.target.value); + } + + handleSearchClick (event) { + const person_clicked = event.currentTarget.firstChild.nextSibling.innerHTML; + fetchUserData(person_clicked, this); + this.setState({ + searchResults : [], + dirtySearch: false + }); + } + + handleFollowersClick (event) { + const person_clicked = event.currentTarget.firstChild.firstChild.nextSibling.innerHTML; + fetchUserData(person_clicked, this); + } + render () { + let profilePage; + if (this.state.currentFocus) { + profilePage = + ; + } + return ( -
-

Welcome To The Exercise

- {this.props.children} +
+
+
+

Search GitHub Users

+
+
+ + +
+ {this.props.children} +
+ {profilePage}
); } + } diff --git a/views/RootView/searchResultView.js b/views/RootView/searchResultView.js new file mode 100644 index 0000000..020ce87 --- /dev/null +++ b/views/RootView/searchResultView.js @@ -0,0 +1,31 @@ +import React from 'react'; + +export default class SearchResultView extends React.Component { + + render () { + + let githubSearchResults; + if (this.props.searchResults.length > 0) { + githubSearchResults = this.props.searchResults.slice(0, 10).map((person) => { + return ( +
  • +
    + +
    {person.login}
    +
    +
    +
  • + ); + }); + } else if (this.props.dirtySearch){ + githubSearchResults = No results. Please search for another user. + } + + return ( +
      + {githubSearchResults} +
    + ); + } + +} \ No newline at end of file diff --git a/views/profileView/centerSection.js b/views/profileView/centerSection.js new file mode 100644 index 0000000..d9fbaf6 --- /dev/null +++ b/views/profileView/centerSection.js @@ -0,0 +1,30 @@ +import React from 'react'; + +export default class CenterSection extends React.Component { + + render () { + let followers; + if (this.props.currentProfileFollowers.length > 0) { + followers = this.props.currentProfileFollowers.map((follower) => { + return ( +
  • +
    + +
    {follower.login}
    +
    +
    +
  • + ) + }) + } else { + followers =
    this user got no followers
    + } + + return ( +
      + {followers} +
    + ) + } + +}; \ No newline at end of file diff --git a/views/profileView/index.js b/views/profileView/index.js new file mode 100644 index 0000000..d73862e --- /dev/null +++ b/views/profileView/index.js @@ -0,0 +1,29 @@ +import React from 'react'; +import LeftSection from './leftSection.js'; +import CenterSection from './centerSection.js'; +import RightSection from './rightSection.js'; + +export default class ProfileView extends React.Component { + + render () { + return ( +
    +
    + +
    +
    +
    Followers
    + +
    +
    +
    Repos
    + +
    +
    + ); + } + +}; \ No newline at end of file diff --git a/views/profileView/leftSection.js b/views/profileView/leftSection.js new file mode 100644 index 0000000..a37e844 --- /dev/null +++ b/views/profileView/leftSection.js @@ -0,0 +1,39 @@ +import React from 'react'; +import {stringifyDate} from '../../utils/utils.js'; + +export default class LeftSection extends React.Component { + + render () { + let company, blog, location, email, bio; + if (this.props.profileInfo.company){ + company =
    {this.props.profileInfo.company}
    ; + }; + if (this.props.profileInfo.blog){ + blog =
    {this.props.profileInfo.blog}
    ; + }; + if (this.props.profileInfo.location){ + location =
    {this.props.profileInfo.location}
    ; + }; + if (this.props.profileInfo.email){ + email =
    {this.props.profileInfo.email}
    ; + }; + if (this.props.profileInfo.bio){ + bio =
    {this.props.profileInfo.bio}
    ; + }; + + return ( +
    + +

    {this.props.profileInfo.login}

    +
    {this.props.profileInfo.name}
    + {company} + {blog} + {location} + {email} + {bio} +
    profile created at: {stringifyDate(this.props.profileInfo.created_at)}
    +
    + ) + } + +} \ No newline at end of file diff --git a/views/profileView/rightSection.js b/views/profileView/rightSection.js new file mode 100644 index 0000000..75305de --- /dev/null +++ b/views/profileView/rightSection.js @@ -0,0 +1,29 @@ +import React from 'react'; + +export default class RightSection extends React.Component { + + render () { + let repos; + if (this.props.currentProfileRepos.length > 0) { + repos = this.props.currentProfileRepos.map((repo) => { + return ( +
  • +
    +
    {repo.name}
    + {repo.stargazers_count} stars +
    +
  • + ) + }) + } else { + repos =
    this user does not have any repos
    + } + + return ( +
      + {repos} +
    + ) + } + +}; \ No newline at end of file