├── .gitignore ├── LICENSE ├── README.md ├── github.gif ├── home.png ├── package.json ├── public ├── images │ └── octocat.svg ├── index.html └── manifest.json ├── src ├── App.js ├── components │ ├── App-Container.js │ ├── Avatar.js │ ├── Contributions.js │ ├── Followers.js │ ├── Following.js │ ├── Issues.js │ ├── LoadingChecker.js │ ├── LoadingIndicator.js │ ├── LoginScreen.js │ ├── MarketPlace.js │ ├── Nav.js │ ├── Overview.js │ ├── Profile.js │ ├── ProfileDetails.js │ ├── ProfileMenu.js │ ├── PullRequests.js │ ├── Repositories.js │ ├── Results.js │ ├── Search.js │ ├── Stars.js │ └── UserMenu.js ├── images │ └── octocat.svg ├── index.css └── index.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | .env 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Paul Fitzgerald 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-Github 💻👩‍💻💽👨‍💻 2 | 3 | This is `React-Github`, a React front end client that communicates with the Github GraphQL API. 4 | 5 | See it in action [here](http://pau1fitz.github.io/react-github). 6 | 7 | Before running the code **locally** you will need to deploy [Heroku Gatekeeper](https://github.com/prose/gatekeeper#deploy-on-heroku) with the appropriate Github client id and client secret. Then run the following: 8 | 9 | ``` 10 | yarn 11 | yarn start 12 | visit http://localhost:3000 13 | ``` 14 | 15 | ![alt text](https://github.com/Pau1fitz/react-github/blob/master/github.gif "Home") 16 | 17 | ### License 18 | 19 | Released under the MIT License. Check [LICENSE.md](https://github.com/Pau1fitz/react-github/blob/master/LICENSE) for more info. 20 | -------------------------------------------------------------------------------- /github.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pau1fitz/github-graphql-react/3ad4b990d6d34520efb3391d55221e7a152d30a0/github.gif -------------------------------------------------------------------------------- /home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pau1fitz/github-graphql-react/3ad4b990d6d34520efb3391d55221e7a152d30a0/home.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-github", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "apollo-boost": "^0.1.6", 7 | "apollo-cache-inmemory": "^1.1.9", 8 | "apollo-client": "^2.2.5", 9 | "apollo-link-context": "^1.0.7", 10 | "apollo-link-http": "^1.5.2", 11 | "github-calendar": "^1.2.1", 12 | "gitstar-components": "^1.0.5", 13 | "graphql": "^0.13.1", 14 | "graphql-tag": "^2.8.0", 15 | "lodash": "^4.17.5", 16 | "moment": "^2.21.0", 17 | "react": "^16.2.0", 18 | "react-apollo": "^2.1.4", 19 | "react-dom": "^16.2.0", 20 | "react-router": "^4.2.0", 21 | "react-router-dom": "^4.2.2", 22 | "react-scripts": "1.1.1", 23 | "styled-components": "^3.1.6" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test --env=jsdom", 29 | "eject": "react-scripts eject" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /public/images/octocat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | React Github 18 | 19 | 20 | 21 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import ApolloClient from 'apollo-boost' 3 | import { ApolloProvider } from 'react-apollo' 4 | import LoginScreen from './components/LoginScreen' 5 | import AppContainer from './components/App-Container' 6 | import { Loading } from 'gitstar-components' 7 | 8 | const STATUS = { 9 | INITIAL: 'initial', 10 | LOADING: 'loading', 11 | FINISHED_LOADING: 'finished_loading', 12 | AUTHENTICATED: 'authenticated' 13 | } 14 | 15 | const AUTH_API_URI = process.env.REACT_APP_AUTH_API_URI 16 | 17 | const client = new ApolloClient({ 18 | uri: 'https://api.github.com/graphql', 19 | request: operation => { 20 | const token = localStorage.getItem('github_token') 21 | if (token) { 22 | operation.setContext({ 23 | headers: { 24 | authorization: `Bearer ${token}` 25 | } 26 | }) 27 | } 28 | } 29 | }) 30 | 31 | class App extends Component { 32 | 33 | state = { 34 | status: STATUS.INITIAL, 35 | token: null 36 | } 37 | 38 | componentDidMount() { 39 | const storedToken = localStorage.getItem('github_token'); 40 | if (storedToken) { 41 | this.setState({ 42 | token: storedToken, 43 | status: STATUS.AUTHENTICATED 44 | }) 45 | return 46 | } 47 | const code = 48 | window.location.href.match(/\?code=(.*)/) && 49 | window.location.href.match(/\?code=(.*)/)[1]; 50 | if (code) { 51 | this.setState({ status: STATUS.LOADING }); 52 | fetch(`${AUTH_API_URI}${code}`) 53 | .then(response => response.json()) 54 | .then(({ token }) => { 55 | localStorage.setItem('github_token', token) 56 | this.setState({ 57 | token, 58 | status: STATUS.FINISHED_LOADING 59 | }) 60 | }) 61 | } 62 | } 63 | render() { 64 | return ( 65 | 66 |
67 | { this.state.status === STATUS.AUTHENTICATED && ( 68 | 69 | )} 70 | 71 |
72 | { this.state.status === STATUS.INITIAL && ( 73 | 74 | )} 75 |
76 | { 79 | if (this.props.status !== STATUS.AUTHENTICATED) { 80 | this.setState({ 81 | status: STATUS.AUTHENTICATED 82 | }) 83 | } 84 | }} 85 | /> 86 |
87 |
88 | ) 89 | } 90 | } 91 | 92 | export default App 93 | -------------------------------------------------------------------------------- /src/components/App-Container.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { graphql } from 'react-apollo' 3 | import gql from 'graphql-tag' 4 | import styled from 'styled-components' 5 | import { Route, Switch } from 'react-router-dom' 6 | 7 | import Nav from './Nav' 8 | import Profile from './Profile' 9 | import Overview from './Overview' 10 | import Repositories from './Repositories' 11 | import Followers from './Followers' 12 | import Following from './Following' 13 | import ProfileMenu from './ProfileMenu' 14 | import PullRequests from './PullRequests' 15 | import Issues from './Issues' 16 | import Stars from './Stars' 17 | import MarketPlace from './MarketPlace' 18 | 19 | const Home = ({ avatarUrl, userFullName, username, location, company, bio, organizations }) => { 20 | return ( 21 | 22 | 23 | 32 | 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 | ) 45 | } 46 | 47 | class App extends Component { 48 | 49 | render() { 50 | const { viewer } = this.props.data 51 | 52 | const avatarUrl = viewer ? viewer.avatarUrl : '' 53 | const userFullName = viewer ? viewer.name : '' 54 | const username = viewer ? viewer.login : '' 55 | const location = viewer ? viewer.location : '' 56 | const company = viewer ? viewer.company : '' 57 | const bio = viewer ? viewer.bio : '' 58 | const organizations = viewer ? viewer.organizations : {} 59 | 60 | return ( 61 |
62 |
82 | ) 83 | } 84 | } 85 | 86 | const ProfileContainer = styled.section` 87 | display: flex; 88 | max-width: 1012px; 89 | margin: 0 auto; 90 | height: 100px; 91 | ` 92 | 93 | const InformationContainer = styled.section` 94 | margin-top: 24px; 95 | ` 96 | 97 | export default graphql(gql` 98 | query user { 99 | viewer { 100 | avatarUrl 101 | name 102 | login 103 | company 104 | location 105 | bio 106 | organizations(first:5) { 107 | edges { 108 | node { 109 | avatarUrl 110 | } 111 | } 112 | } 113 | } 114 | } 115 | `)(App) 116 | -------------------------------------------------------------------------------- /src/components/Avatar.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { gql } from "apollo-boost" 3 | import { Query } from "react-apollo" 4 | import styled from 'styled-components' 5 | 6 | const GET_AVATAR = gql` 7 | query { 8 | viewer { 9 | avatarUrl 10 | } 11 | } 12 | ` 13 | 14 | class UserAvatar extends React.Component { 15 | render() { 16 | return ( 17 | 18 | {({ loading, error, data }) => { 19 | if (loading) return
Loading...
; 20 | if (error) return
Error :(
; 21 | 22 | return ; 23 | }} 24 |
25 | ) 26 | } 27 | } 28 | 29 | 30 | const ProfilePic = styled.img` 31 | border-radius: 3px; 32 | height: 20px; 33 | width: 20px; 34 | cursor: pointer; 35 | margin-right: 4px; 36 | margin-top: 8px; 37 | ` 38 | 39 | export default UserAvatar 40 | -------------------------------------------------------------------------------- /src/components/Contributions.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { graphql } from 'react-apollo' 3 | import gql from 'graphql-tag' 4 | import styled from 'styled-components' 5 | 6 | const Contributions = ({ data: { viewer }}) => { 7 | 8 | const repos = viewer && viewer.repositories ? viewer.repositories.edges.map(repo => ( 9 | 10 | { repo.node.name } 11 | { repo.node.description } 12 | { repo.node.languages.edges[0].node.name } { repo.node.stargazers.totalCount } { repo.node.forkCount } 13 | 14 | ) 15 | ) : [] 16 | 17 | return ( 18 | 19 | { repos } 20 | 21 | ) 22 | } 23 | 24 | const RepoContainer = styled.div` 25 | display: flex; 26 | flex-wrap: wrap; 27 | justify-content: space-between; 28 | ` 29 | 30 | const RepoCard = styled.div` 31 | border: 1px #d1d5da solid; 32 | padding: 16px; 33 | width: 362px; 34 | margin-bottom: 16px; 35 | ` 36 | 37 | const RepoDescription = styled.p` 38 | font-size: 12px; 39 | color: #586069; 40 | ` 41 | 42 | const RepoLink = styled.a` 43 | font-weight: 600; 44 | font-size: 14px; 45 | color: #0366d6; 46 | ` 47 | 48 | const RepoDetails = styled.span` 49 | color: #586069; 50 | font-size: 12px; 51 | ` 52 | 53 | const Icon = styled.i` 54 | margin-left: 16px; 55 | ` 56 | 57 | export default graphql(gql` 58 | query { 59 | viewer { 60 | repositories(first:6, orderBy: {field: STARGAZERS, direction: DESC}) { 61 | totalCount 62 | edges { 63 | node { 64 | name 65 | description 66 | languages(first: 1, orderBy: {field: SIZE, direction: DESC}) { 67 | edges { 68 | node { 69 | name 70 | } 71 | } 72 | } 73 | forkCount 74 | stargazers { 75 | totalCount 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | `)(Contributions) -------------------------------------------------------------------------------- /src/components/Followers.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { graphql } from 'react-apollo' 3 | import gql from 'graphql-tag' 4 | import styled from 'styled-components' 5 | import LoadingIndicator from './LoadingIndicator' 6 | 7 | const Followers = ({ data: { viewer }}) => { 8 | 9 | const followers = viewer && viewer.followers ? viewer.followers.edges.map((follower, i) => { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | { follower.node.name } 17 | { follower.node.login } 18 | 19 | { follower.node.bio } 20 | {follower.node.location && ( 21 |
22 | 23 | { follower.node.location } 24 |
25 | )} 26 | 27 |
28 | 29 |
30 | ) 31 | }) : 32 | 33 | return ( 34 |
35 | { followers } 36 |
37 | ) 38 | } 39 | 40 | 41 | const Icon = styled.i` 42 | font-size: 18px; 43 | margin-left: 4px; 44 | ` 45 | 46 | const FollowersContainer = styled.div` 47 | display: flex; 48 | ` 49 | 50 | const FollowersInfoContainer = styled.div` 51 | font-size: 12px; 52 | ` 53 | 54 | const FollowersName = styled.div` 55 | display: flex; 56 | align-items: flex-end; 57 | margin-bottom: 4px; 58 | ` 59 | 60 | const FollowersImage = styled.img` 61 | height: 50px; 62 | width: 50px; 63 | border-radius: 3px; 64 | margin-right: 5px; 65 | ` 66 | 67 | const FollowersCard = styled.div` 68 | border-bottom: 1px #d1d5da solid; 69 | padding: 16px; 70 | margin-bottom: 16px; 71 | ` 72 | 73 | const FollowerName = styled.p` 74 | font-size: 16px; 75 | color: #24292e; 76 | padding-left: 4px; 77 | margin-bottom: 0; 78 | ` 79 | 80 | const FollowerLogin = styled.p` 81 | font-size: 14px; 82 | color: #586069; 83 | padding-left: 4px; 84 | position: relative; 85 | margin-bottom: 0; 86 | top: -1px; 87 | ` 88 | 89 | const FollowerLocation = styled.p` 90 | font-size: 14px; 91 | color: #586069; 92 | padding-left: 4px; 93 | display: inline-block; 94 | margin-bottom: 4px; 95 | ` 96 | 97 | const FollowerBio = styled.p` 98 | font-size: 14px; 99 | color: #586069; 100 | padding-left: 4px; 101 | margin-bottom: 4px; 102 | ` 103 | 104 | export default graphql(gql` 105 | query { 106 | viewer { 107 | followers(first:100) { 108 | totalCount 109 | edges { 110 | node { 111 | avatarUrl 112 | name 113 | login 114 | location 115 | bio 116 | 117 | } 118 | } 119 | } 120 | } 121 | } 122 | `)(Followers) 123 | -------------------------------------------------------------------------------- /src/components/Following.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { graphql } from 'react-apollo' 3 | import gql from 'graphql-tag' 4 | import styled from 'styled-components' 5 | import LoadingIndicator from './LoadingIndicator' 6 | 7 | const Following = ({ data: { viewer }}) => { 8 | 9 | const follow = viewer && viewer.following ? viewer.following.edges.map((follower, i) => { 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | { follower.node.name } 21 | { follower.node.login } 22 | 23 | 24 | { follower.node.bio } 25 | {follower.node.location && ( 26 |
27 | 28 | { follower.node.location } 29 |
30 | )} 31 | 32 |
33 | 34 |
35 | ) 36 | }) : 37 | 38 | return ( 39 |
40 | { follow } 41 |
42 | ) 43 | } 44 | 45 | 46 | const Icon = styled.i` 47 | font-size: 18px; 48 | margin-left: 4px; 49 | ` 50 | 51 | const FollowersContainer = styled.div` 52 | display: flex; 53 | ` 54 | 55 | const FollowersInfoContainer = styled.div` 56 | font-size: 12px; 57 | ` 58 | 59 | const FollowersName = styled.div` 60 | display: flex; 61 | align-items: flex-end; 62 | margin-bottom: 4px; 63 | ` 64 | 65 | const FollowersImage = styled.img` 66 | height: 50px; 67 | width: 50px; 68 | border-radius: 3px; 69 | margin-right: 5px; 70 | ` 71 | 72 | const FollowersCard = styled.div` 73 | border-bottom: 1px #d1d5da solid; 74 | padding: 16px; 75 | margin-bottom: 16px; 76 | ` 77 | 78 | const FollowerName = styled.p` 79 | font-size: 16px; 80 | color: #24292e; 81 | padding-left: 4px; 82 | margin-bottom: 0; 83 | ` 84 | 85 | const FollowerLogin = styled.p` 86 | font-size: 14px; 87 | margin-bottom: 0; 88 | color: #586069; 89 | padding-left: 4px; 90 | position: relative; 91 | top: -1px; 92 | ` 93 | 94 | const FollowerLocation = styled.p` 95 | font-size: 14px; 96 | color: #586069; 97 | padding-left: 4px; 98 | display: inline-block; 99 | margin-bottom: 4px; 100 | ` 101 | 102 | const FollowerBio = styled.p` 103 | font-size: 14px; 104 | color: #586069; 105 | padding-left: 4px; 106 | margin-bottom: 4px; 107 | ` 108 | 109 | export default graphql(gql` 110 | query { 111 | viewer { 112 | following(first:100) { 113 | totalCount 114 | edges { 115 | node { 116 | avatarUrl 117 | name 118 | login 119 | location 120 | bio 121 | 122 | } 123 | } 124 | } 125 | } 126 | } 127 | `)(Following) 128 | -------------------------------------------------------------------------------- /src/components/Issues.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { graphql } from 'react-apollo' 3 | import gql from 'graphql-tag' 4 | import styled from 'styled-components' 5 | import moment from 'moment' 6 | 7 | const Issues = ({ data: { viewer }}) => { 8 | 9 | const issues = viewer && viewer.issues ? viewer.issues.edges.map(issue => ( 10 | 11 | 12 | 18 | ) 19 | ) : [] 20 | 21 | const openIssues = viewer && viewer.issues ? viewer.issues.edges.filter(pr => { 22 | return pr.node.state === 'OPEN' 23 | }).length : null 24 | 25 | const closedIssues = viewer && viewer.issues ? viewer.issues.edges.filter(pr => { 26 | return pr.node.state === 'CLOSED' 27 | }).length : null 28 | 29 | return ( 30 |
31 | 32 | { openIssues ? `${openIssues} Open` : null}{ closedIssues ? `${closedIssues} Closed` : null} 33 | 34 | 35 | { issues } 36 | 37 |
38 | ) 39 | } 40 | 41 | const IssueContainer = styled.section` 42 | width: 980px; 43 | margin: 0 auto; 44 | border-left: 1px solid #e1e4e8; 45 | border-right: 1px solid #e1e4e8; 46 | border-top: 1px solid #e1e4e8; 47 | ` 48 | 49 | const IssueCountBG = styled.div` 50 | width: 980px; 51 | margin: 0 auto; 52 | background: #f6f8fa; 53 | border-top: 1px solid #e1e4e8; 54 | border-left: 1px solid #e1e4e8; 55 | border-right: 1px solid #e1e4e8; 56 | border-radius: 3px 3px 0 0; 57 | padding-top: 13px; 58 | padding-bottom: 13px; 59 | padding-left: 16px; 60 | ` 61 | const IssueCount = styled.span` 62 | font-size: 14px; 63 | :last-child { 64 | margin-left: 10px; 65 | } 66 | ` 67 | 68 | const IssueCard = styled.div` 69 | display: flex; 70 | border-bottom: 1px solid #e1e4e8; 71 | ` 72 | 73 | const IssueInfo = styled.p` 74 | font-size: 12px; 75 | color: #586069; 76 | ` 77 | 78 | const IssueDetails = styled.div` 79 | padding: 8px; 80 | ` 81 | 82 | const Icon = styled.i` 83 | color: #28a745; 84 | font-size: 20px; 85 | padding-left: 16px; 86 | padding-top: 8px; 87 | ` 88 | 89 | const NameWithOwner = styled.span` 90 | color: #586069; 91 | padding-right: 4px; 92 | font-size: 16px; 93 | ` 94 | 95 | export default graphql(gql` 96 | query { 97 | viewer { 98 | issues(first: 10) { 99 | edges { 100 | node { 101 | publishedAt 102 | state 103 | title 104 | author { 105 | login 106 | } 107 | repository { 108 | nameWithOwner 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | `)(Issues) -------------------------------------------------------------------------------- /src/components/LoadingChecker.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | const STATUS = { 4 | INITIAL: "initial", 5 | LOADING: "loading", 6 | FINISHED_LOADING: "finished_loading", 7 | AUTHENTICATED: "authenticated" 8 | } 9 | 10 | class LoadingChecker extends React.Component { 11 | render() { 12 | return ( 13 |
14 | {this.props.status !== STATUS.AUTHENTICATED && ( 15 |
16 |
17 | )} 18 |
/> 19 |
20 |
21 | ) 22 | } 23 | } 24 | 25 | export default LoadingChecker 26 | -------------------------------------------------------------------------------- /src/components/LoadingIndicator.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | const LoadingIndicator = () => ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | 36 | const OctocatContainer = styled.div` 37 | display: flex; 38 | align-items: center; 39 | justify-content: center; 40 | width: 100%; 41 | ` 42 | 43 | export default LoadingIndicator 44 | -------------------------------------------------------------------------------- /src/components/LoginScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components' 3 | 4 | const CLIENT_ID = process.env.REACT_APP_CLIENT_ID 5 | const REDIRECT_URI = process.env.REACT_APP_REDIRECT_URI 6 | 7 | const LoginScreen = () => { 8 | 9 | return ( 10 | 11 | 15 | 16 | 17 | React Github 18 | 19 | Login 20 | 21 | 22 | ) 23 | } 24 | 25 | const LoginContainer = styled.div` 26 | display: flex; 27 | background: #24292e; 28 | color: #fff; 29 | justify-content: center; 30 | align-items: center; 31 | height: 100vh; 32 | flex-direction: column; 33 | position: fixed; 34 | width: 100%; 35 | ` 36 | 37 | const Title = styled.p` 38 | color: #fff; 39 | font-size: 24px; 40 | font-weight: 600; 41 | margin: 20px 0 10px 0; 42 | ` 43 | 44 | const LoginLink = styled.a` 45 | color: #fff; 46 | font-size: 16px; 47 | &:hover { 48 | color: #ccc; 49 | } 50 | ` 51 | const Logo = styled.svg` 52 | fill: #fff; 53 | ` 54 | 55 | export default LoginScreen 56 | 57 | -------------------------------------------------------------------------------- /src/components/MarketPlace.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { graphql } from 'react-apollo' 3 | import gql from 'graphql-tag' 4 | import styled from 'styled-components' 5 | import LoadingIndicator from './LoadingIndicator' 6 | 7 | const MarketPlace = ({ data }) => { 8 | 9 | let slicedListings 10 | 11 | const listings = data.marketplaceListings ? data.marketplaceListings.nodes.map(listing => { 12 | return ( 13 | 14 | 15 | 16 | 17 | {listing.name} 18 | 19 | {listing.shortDescription} 20 | 21 | 22 | ) 23 | }) : 24 | 25 | 26 | if(data.marketplaceListings) { 27 | slicedListings = data.marketplaceListings.nodes.slice(0, 4); 28 | slicedListings = slicedListings.map(l => { 29 | return ( 30 | 31 | 32 | 33 |

{ l.name }

34 |
35 |
36 | ) 37 | }) 38 | } 39 | 40 | return ( 41 |
42 | 43 | Github MarketPlace 44 | Tools to build on and improve your workflow 45 | 46 | {slicedListings} 47 | 48 | 49 | 50 | 51 | {listings} 52 | 53 | 54 |
55 | ) 56 | } 57 | 58 | const MarketPlaceContainer = styled.div` 59 | width: 980px; 60 | margin: 0 auto; 61 | ` 62 | 63 | const ListingContainer = styled.div` 64 | display: flex; 65 | flex-wrap: wrap; 66 | justify-content: space-between; 67 | ` 68 | 69 | const LargeItemBox = styled.div` 70 | height: 240px; 71 | width: 300px; 72 | background: ${props => props.color }; 73 | align-items: center; 74 | justify-content: center; 75 | display: flex; 76 | flex-direction: column; 77 | ` 78 | 79 | const LargeItemImage = styled.img` 80 | height: 75px; 81 | width: 75px; 82 | border-radius: 50%; 83 | margin-bottom: 10px; 84 | position: relative; 85 | top: 0; 86 | transition: top 0.15s ease-in, box-shadow 0.12s ease-in; 87 | &:hover { 88 | top: -10px; 89 | } 90 | ` 91 | const ItemLink = styled.a` 92 | color: #24292e; 93 | font-size: 24px; 94 | font-weight: 600; 95 | text-align: center; 96 | ` 97 | 98 | const MarketPlaceItemContainer = styled.div` 99 | display: flex; 100 | width: 300px; 101 | margin-bottom: 10px; 102 | ` 103 | 104 | const MarketPlaceItemInfo = styled.div` 105 | margin-left: 10px; 106 | ` 107 | 108 | const LargeItemContainer = styled.div` 109 | display: flex; 110 | justify-content: space-around; 111 | margin-top: 20px; 112 | ` 113 | 114 | const MarketPlaceJumbotron = styled.div` 115 | background-color: #2f363d; 116 | background-image: url(https://www.github.com/images/modules/marketplace/bg-hero.svg); 117 | background-position: center top; 118 | background-size: cover; 119 | padding-top: 40px !important; 120 | padding-bottom: 40px !important; 121 | margin-top: -24px; 122 | margin-bottom: 20px; 123 | ` 124 | 125 | const Title = styled.h2` 126 | font-size: 54px; 127 | font-weight: 300; 128 | text-align: center; 129 | color: #fff; 130 | margin-bottom: 16px; 131 | ` 132 | 133 | const SubTitle = styled.h2` 134 | font-size: 26px; 135 | font-weight: 300; 136 | text-align: center; 137 | color: #fff; 138 | opacity: 0.5; 139 | ` 140 | 141 | const ShortDescription = styled.p` 142 | color: #6a737d; 143 | font-size: 14px; 144 | ` 145 | 146 | const Name = styled.p` 147 | color: #0366d6; 148 | ` 149 | 150 | const MarketPlaceImage = styled.img` 151 | width: 50px; 152 | height: 50px; 153 | border-radius: 50%; 154 | ` 155 | 156 | export default graphql(gql` 157 | query { 158 | marketplaceListings(first: 10) { 159 | nodes { 160 | companyUrl 161 | logoUrl 162 | logoBackgroundColor 163 | name 164 | pricingUrl 165 | shortDescription 166 | } 167 | } 168 | } 169 | `)(MarketPlace) 170 | 171 | 172 | -------------------------------------------------------------------------------- /src/components/Nav.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled from 'styled-components' 3 | import { NavLink } from 'react-router-dom' 4 | import { withRouter } from 'react-router-dom' 5 | import UserMenu from './UserMenu' 6 | import Search from './Search' 7 | import Avatar from "./Avatar" 8 | 9 | const activeStyles = () => ( 10 | { 11 | fontWeight: '600', 12 | color: '#fff' 13 | } 14 | ) 15 | 16 | const linkstyles = () => ( 17 | { 18 | color: 'rgba(255,255,255,0.75)', 19 | textDecoration:'none' 20 | } 21 | ) 22 | 23 | class Nav extends Component { 24 | 25 | constructor(props) { 26 | super(props) 27 | this.state = { 28 | menuOpen: false 29 | } 30 | } 31 | 32 | componentDidMount() { 33 | document.addEventListener('click', this.handleClickOutside.bind(this), true); 34 | } 35 | 36 | componentWillUnmount() { 37 | document.removeEventListener('click', this.handleClickOutside.bind(this), true); 38 | } 39 | 40 | handleClickOutside(e) { 41 | 42 | const domNode = document.getElementById('dropdown-menu'); 43 | 44 | if (domNode && !domNode.contains(e.target)) { 45 | this.setState({ 46 | menuOpen: false 47 | }); 48 | } 49 | } 50 | 51 | openMenu = () => { 52 | this.setState({ 53 | menuOpen: true 54 | }) 55 | } 56 | 57 | closeMenu = () => { 58 | this.setState({ 59 | menuOpen: false 60 | }) 61 | } 62 | 63 | render() { 64 | 65 | const { menuOpen } = this.state 66 | const { username } = this.props 67 | 68 | return ( 69 | 70 |
71 | 72 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 88 | Pull Requests 89 | 90 | 91 | 95 | Issues 96 | 97 | 98 | 102 | Marketplace 103 | 104 | 105 | Explore 106 | 107 | 108 | 109 | 110 | 111 | {menuOpen && ( 112 | 117 | )} 118 | 119 |
120 | 121 |
122 | ) 123 | 124 | } 125 | } 126 | 127 | const HeaderContainer = styled.section` 128 | color: rgba(255,255,255,0.75); 129 | background-color: #24292e; 130 | margin-bottom: 24px; 131 | position: relative; 132 | ` 133 | 134 | const Header = styled.header` 135 | max-width: 1012px; 136 | margin: 0 auto; 137 | display: flex; 138 | padding-top: 12px; 139 | padding-bottom: 12px; 140 | align-items: center; 141 | ` 142 | 143 | const Logo = styled.svg` 144 | fill: #fff; 145 | ` 146 | 147 | const NavContainer = styled.div` 148 | display: flex; 149 | ` 150 | 151 | const NavItem = styled.li` 152 | padding: 0 12px; 153 | background-color: #24292e; 154 | list-style: none; 155 | font-weight: 600; 156 | font-size: 14px; 157 | text-decoration: none; 158 | &:hover { 159 | color: #fff; 160 | } 161 | ` 162 | 163 | const UserSection = styled.div` 164 | position: relative; 165 | ` 166 | 167 | const DropDownCaret = styled.span` 168 | display: inline-block; 169 | width: 0; 170 | height: 0; 171 | vertical-align: middle; 172 | content: ""; 173 | border: 4px solid; 174 | border-right-color: transparent; 175 | border-bottom-color: transparent; 176 | border-left-color: transparent; 177 | cursor: pointer; 178 | ` 179 | 180 | export default withRouter(Nav) 181 | -------------------------------------------------------------------------------- /src/components/Overview.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { graphql } from 'react-apollo' 3 | import gql from 'graphql-tag' 4 | import styled from 'styled-components' 5 | import LoadingIndicator from './LoadingIndicator' 6 | import GitHubCalendar from 'github-calendar' 7 | 8 | class Overview extends Component { 9 | 10 | componentDidMount() { 11 | if(this.props.data && this.props.data.viewer) { 12 | new GitHubCalendar('.calendar', this.props.data.viewer.login) 13 | } 14 | } 15 | 16 | componentWillReceiveProps(nextProps) { 17 | new GitHubCalendar('.calendar', nextProps.data.viewer.login) 18 | } 19 | 20 | render() { 21 | 22 | const { viewer } = this.props.data 23 | 24 | const repos = viewer && viewer.repositories ? viewer.repositories.edges.map((repo , i) => { 25 | // Only show 6 repos 26 | if(i < 6) { 27 | return ( 28 | 29 | { repo.node.name } 30 | { repo.node.description } 31 | 32 | 33 | { repo.node.languages.edges && repo.node.languages.edges[0] && repo.node.languages.edges[0].node.name && repo.node.languages.edges[0].node.name } 34 | 35 | 36 | ) 37 | } else { 38 | return null 39 | } 40 | }) : 41 | 42 | return ( 43 |
44 | { repos.length > 1 && ( 45 | Popular Repositories 46 | )} 47 | 48 | { repos } 49 | 50 | 51 | 52 |
53 | 54 | 55 |
56 | ) 57 | } 58 | } 59 | 60 | const RepoContainer = styled.div` 61 | display: flex; 62 | flex-wrap: wrap; 63 | justify-content: space-between; 64 | ` 65 | 66 | const RepoCard = styled.div` 67 | border: 1px #d1d5da solid; 68 | padding: 16px; 69 | width: 362px; 70 | margin-bottom: 16px; 71 | ` 72 | 73 | const RepoDescription = styled.p` 74 | font-size: 12px; 75 | color: #586069; 76 | margin: 4px 0 10px 0; 77 | ` 78 | 79 | const RepoInfoContainer = styled.div` 80 | display: flex; 81 | ` 82 | 83 | const Circle = styled.div` 84 | height: 12px; 85 | width: 12px; 86 | border-radius: 50%; 87 | background: #f1e05a; 88 | margin-right: 5px; 89 | top: 2px; 90 | position: relative; 91 | ` 92 | 93 | const OverviewTitle = styled.p` 94 | color: #24292e; 95 | font-size: 16px; 96 | margin-bottom: 8px; 97 | ` 98 | 99 | const RepoLink = styled.a` 100 | font-weight: 600; 101 | font-size: 14px; 102 | color: #0366d6; 103 | cursor: pointer; 104 | ` 105 | 106 | const RepoDetails = styled.p` 107 | color: #586069; 108 | font-size: 12px; 109 | margin: 0; 110 | ` 111 | 112 | const Icon = styled.i` 113 | margin-left: 16px; 114 | ` 115 | 116 | const CalendarContainer = styled.div` 117 | position: relative; 118 | ` 119 | 120 | export default graphql(gql` 121 | query { 122 | viewer { 123 | login 124 | repositories(first:100, orderBy: {field: STARGAZERS, direction: DESC}) { 125 | totalCount 126 | edges { 127 | node { 128 | name 129 | description 130 | languages(first: 1, orderBy: {field: SIZE, direction: DESC}) { 131 | edges { 132 | node { 133 | name 134 | } 135 | } 136 | } 137 | forkCount 138 | stargazers { 139 | totalCount 140 | } 141 | } 142 | } 143 | } 144 | } 145 | } 146 | `)(Overview) 147 | -------------------------------------------------------------------------------- /src/components/Profile.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | const Profile = ({ avatarUrl, userFullName, username, company, location, bio, organizations }) => { 5 | 6 | const organsiationList = organizations && organizations.edges && organizations.edges.length > 0 ? 7 | organizations.edges.map(org => { 8 | return 9 | }) : [] 10 | 11 | return ( 12 | 13 | { avatarUrl === '' ? : } 14 | 15 | { userFullName } 16 | { username } 17 | 18 | 19 | 20 | { bio ? bio : '' } 21 | 22 | 23 | 24 | 25 | 26 | {company && ( 27 |
28 |
30 | )} 31 | {location && ( 32 |
33 |
35 | )} 36 |
37 | 38 | {organsiationList.length > 0 && ( 39 |
40 | 41 | Organizations 42 | 43 |
44 | )} 45 |
46 | ) 47 | } 48 | 49 | 50 | const ProfileSection = styled.section` 51 | padding-right: 20px; 52 | ` 53 | 54 | const NameSection = styled.div` 55 | padding: 16px 0; 56 | ` 57 | 58 | const LocationSection = styled.div` 59 | padding: 16px 0; 60 | ` 61 | 62 | const ProfileDivider = styled.div` 63 | height: 1px; 64 | margin: 8px 1px; 65 | background-color: #e1e4e8; 66 | ` 67 | 68 | const Organization = styled.p` 69 | margin: 0; 70 | font-weight: 600; 71 | font-size: 16px; 72 | ` 73 | 74 | const Avatar = styled.img` 75 | width: 35px; 76 | height: 35px; 77 | border-radius: 3px; 78 | margin-top: 2px; 79 | ` 80 | 81 | const UsersFullName = styled.p` 82 | font-weight: 600; 83 | font-size: 26px; 84 | line-height: 30px; 85 | margin: 0; 86 | ` 87 | 88 | const UsersName = styled.p` 89 | font-size: 20px; 90 | font-style: normal; 91 | font-weight: 300; 92 | line-height: 24px; 93 | color: #666; 94 | margin: 0; 95 | ` 96 | 97 | const ProfilePic = styled.img` 98 | border-radius: 6px; 99 | height: 230px; 100 | width: 230px; 101 | ` 102 | 103 | const Placeholder = styled.div` 104 | border-radius: 6px; 105 | height: 230px; 106 | width: 230px; 107 | background: #fff; 108 | ` 109 | 110 | const Organisation = styled.p` 111 | font-weight: 600; 112 | font-size: 14px; 113 | margin: 0; 114 | ` 115 | 116 | const Location = styled.p` 117 | font-size: 14px; 118 | margin: 0; 119 | ` 120 | 121 | const Icon = styled.i` 122 | float: left; 123 | margin-right: 6px; 124 | margin-top: 3px; 125 | ` 126 | 127 | const BioContainer = styled.div` 128 | margin-bottom: 12px; 129 | max-width: 230px; 130 | font-size: 14px; 131 | color: #6a737d; 132 | ` 133 | export default Profile 134 | -------------------------------------------------------------------------------- /src/components/ProfileDetails.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | const ProfileDetails = ({ avatarUrl, userFullName, username, company, location }) => ( 5 | 6 | 7 | { userFullName } 8 | { username } 9 | { company } 10 | { location } 11 | 12 | ) 13 | 14 | 15 | const ProfileSection = styled.section` 16 | padding-right: 16px; 17 | ` 18 | 19 | const UsersFullName = styled.p` 20 | font-weight: 600; 21 | font-size: 26px; 22 | line-height: 30px; 23 | ` 24 | 25 | const UsersName = styled.p` 26 | font-size: 20px; 27 | font-style: normal; 28 | font-weight: 300; 29 | line-height: 24px; 30 | color: #666; 31 | ` 32 | 33 | const ProfilePic = styled.img` 34 | border-radius: 6px; 35 | height: 230px; 36 | width: 230px; 37 | ` 38 | 39 | const Organisation = styled.p` 40 | font-weight: 600; 41 | font-size: 14px; 42 | ` 43 | 44 | const Location = styled.p` 45 | font-size: 14px; 46 | ` 47 | 48 | export default ProfileDetails 49 | -------------------------------------------------------------------------------- /src/components/ProfileMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { graphql } from 'react-apollo' 4 | import gql from 'graphql-tag' 5 | import { NavLink } from 'react-router-dom' 6 | 7 | const activeStyles = () => ( 8 | { 9 | fontWeight: '600', 10 | borderBottom: '2px solid #e36209', 11 | color: '#24292e' 12 | } 13 | ) 14 | 15 | const Linkstyles = () => ( 16 | { 17 | padding:'16px 8px', 18 | marginRight: '16px', 19 | fontSize: '14px', 20 | lineHeight: '1.5', 21 | color: '#586069', 22 | textAlign: 'center', 23 | textDecoration:'none' 24 | } 25 | ) 26 | 27 | const ProfileMenu = ({data: { viewer }}) => ( 28 | 29 | 69 | ) 70 | 71 | const Nav = styled.nav` 72 | border-bottom: solid 1px #d1d5da; 73 | padding-bottom: 14px; 74 | ` 75 | 76 | const Counter = styled.span` 77 | padding: 2px 5px; 78 | font-size: 12px; 79 | font-weight: 600; 80 | line-height: 1; 81 | color: #586069; 82 | background-color: rgba(27,31,35,0.08); 83 | border-radius: 20px; 84 | margin-left: 6px; 85 | ` 86 | 87 | export default graphql(gql` 88 | query { 89 | viewer { 90 | repositories { 91 | totalCount 92 | } 93 | followers { 94 | totalCount 95 | } 96 | following { 97 | totalCount 98 | } 99 | starredRepositories{ 100 | totalCount 101 | } 102 | } 103 | } 104 | `)(ProfileMenu) -------------------------------------------------------------------------------- /src/components/PullRequests.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { graphql } from 'react-apollo' 3 | import gql from 'graphql-tag' 4 | import styled from 'styled-components' 5 | import moment from 'moment' 6 | 7 | const PullRequests = ({ data: { viewer }}) => { 8 | 9 | const prs = viewer && viewer.pullRequests ? viewer.pullRequests.edges.map(pr => ( 10 | 11 | 12 | 18 | ) 19 | ) : [] 20 | 21 | const openPRs = viewer && viewer.pullRequests ? viewer.pullRequests.edges.filter(pr => { 22 | return pr.node.state === 'OPEN' 23 | }).length : null 24 | 25 | const closedPRs = viewer && viewer.pullRequests ? viewer.pullRequests.edges.filter(pr => { 26 | return pr.node.state === 'CLOSED' 27 | }).length : null 28 | 29 | return ( 30 |
31 | 32 | { openPRs ? `${openPRs} Open` : null}{ openPRs ? `${closedPRs} Closed` : null} 33 | 34 | 35 | { prs } 36 | 37 |
38 | ) 39 | } 40 | 41 | const PRContainer = styled.section` 42 | width: 980px; 43 | margin: 0 auto; 44 | border-left: 1px solid #e1e4e8; 45 | border-right: 1px solid #e1e4e8; 46 | border-top: 1px solid #e1e4e8; 47 | ` 48 | 49 | const PRCountBG = styled.div` 50 | width: 980px; 51 | margin: 0 auto; 52 | background: #f6f8fa; 53 | border-top: 1px solid #e1e4e8; 54 | border-left: 1px solid #e1e4e8; 55 | border-right: 1px solid #e1e4e8; 56 | border-radius: 3px 3px 0 0; 57 | padding-top: 13px; 58 | padding-bottom: 13px; 59 | padding-left: 16px; 60 | ` 61 | const PRCount = styled.span` 62 | font-size: 14px; 63 | :last-child { 64 | margin-left: 10px; 65 | } 66 | ` 67 | 68 | const PRCard = styled.div` 69 | display: flex; 70 | border-bottom: 1px solid #e1e4e8; 71 | ` 72 | const PRDetailsContainer = styled.div` 73 | padding: 8px; 74 | ` 75 | const PRDetails = styled.p` 76 | font-size: 12px; 77 | color: #586069; 78 | ` 79 | 80 | const Icon = styled.i` 81 | color: #28a745; 82 | font-size: 20px; 83 | padding-left: 16px; 84 | padding-top: 8px; 85 | ` 86 | 87 | const NameWithOwner = styled.span` 88 | color: #586069; 89 | padding-right: 4px; 90 | font-size: 16px; 91 | ` 92 | 93 | export default graphql(gql` 94 | query { 95 | viewer { 96 | pullRequests(first: 100) { 97 | edges { 98 | node { 99 | publishedAt 100 | state 101 | title 102 | author { 103 | login 104 | } 105 | repository { 106 | nameWithOwner 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } 113 | `)(PullRequests) -------------------------------------------------------------------------------- /src/components/Repositories.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { graphql } from 'react-apollo' 3 | import gql from 'graphql-tag' 4 | import styled from 'styled-components' 5 | import moment from 'moment' 6 | import LoadingIndicator from './LoadingIndicator' 7 | 8 | class Repo extends Component { 9 | 10 | state = { 11 | repos: [], 12 | filteredRepos: [], 13 | filtered: false 14 | } 15 | 16 | componentDidMount() { 17 | if(this.props.data && this.props.data.viewer) { 18 | this.setState({ 19 | login: this.props.data.viewer.login, 20 | repos: this.props.data.viewer.repositories.edges 21 | }) 22 | } 23 | } 24 | 25 | componentWillReceiveProps(nextProps) { 26 | if(nextProps.data.viewer.repositories) { 27 | this.setState({ 28 | login: nextProps.data.viewer.login, 29 | repos: nextProps.data.viewer.repositories.edges 30 | }) 31 | } 32 | } 33 | 34 | searchRepos = (e) => { 35 | 36 | const repos = this.state.repos.filter(repo => { 37 | if(repo.node.name.indexOf(e.target.value) > -1) { 38 | return repo 39 | } else { 40 | return null 41 | } 42 | }) 43 | 44 | this.setState({ 45 | filteredRepos: repos, 46 | filtered: true 47 | }) 48 | } 49 | 50 | render() { 51 | 52 | const { repos, login, filteredRepos, filtered } = this.state; 53 | 54 | const visibleRepos = filtered ? filteredRepos : repos 55 | 56 | const repositories = repos.length > 0 ? visibleRepos.map((repo, i) => { 57 | return ( 58 | 59 | { repo.node.name } 60 | { repo.node.description } 61 | 62 | 63 | { repo.node.languages.edges && repo.node.languages.edges[0] && repo.node.languages.edges[0].node.name ? repo.node.languages.edges[0].node.name : null} { repo.node.stargazers.totalCount } { repo.node.forkCount } 64 | { moment(repo.node.updatedAt).fromNow()} 65 | 66 | 67 | ) 68 | }) : 69 | 70 | return ( 71 |
72 | { repos.length > 0 73 | && ( 74 | 75 | 80 | 81 | 82 | ) 83 | } 84 | { repositories } 85 |
86 | ) 87 | } 88 | } 89 | 90 | 91 | const RepoCard = styled.div` 92 | border-bottom: 1px #d1d5da solid; 93 | padding: 16px; 94 | margin-bottom: 16px; 95 | ` 96 | 97 | const SearchContainer = styled.div` 98 | border-bottom: 1px solid #d1d5da; 99 | padding-bottom: 16px; 100 | 101 | ` 102 | 103 | const SearchBox = styled.input` 104 | min-height: 34px; 105 | width: 300px; 106 | font-size: 14px; 107 | padding: 6px 8px; 108 | background-color: #fff; 109 | background-repeat: no-repeat; 110 | background-position: right 8px center; 111 | border: 1px solid #d1d5da; 112 | border-radius: 3px; 113 | outline: none; 114 | box-shadow: inset 0 1px 2px rgba(27,31,35,0.075); 115 | ` 116 | 117 | const Date = styled.p` 118 | font-size: 12px; 119 | color: #586069; 120 | margin-left: 10px; 121 | margin-bottom: 0; 122 | ` 123 | 124 | const InfoContainer = styled.div` 125 | display: flex; 126 | ` 127 | 128 | const Circle = styled.div` 129 | height: 12px; 130 | width: 12px; 131 | border-radius: 50%; 132 | background: #f1e05a; 133 | margin-right: 5px; 134 | top: 2px; 135 | position: relative; 136 | ` 137 | 138 | const RepoDescription = styled.p` 139 | font-size: 14px; 140 | color: #586069; 141 | margin: 4px 0 10px 0; 142 | ` 143 | 144 | const RepoLink = styled.a` 145 | font-weight: 600; 146 | color: #0366d6; 147 | cursor: pointer; 148 | font-size: 20px; 149 | ` 150 | 151 | const RepoDetails = styled.span` 152 | color: #586069; 153 | font-size: 12px; 154 | margin-bottom: 0; 155 | ` 156 | 157 | const Icon = styled.i` 158 | margin-left: 16px; 159 | ` 160 | 161 | export default graphql(gql` 162 | query { 163 | viewer { 164 | login 165 | repositories(first: 100, orderBy: {field: STARGAZERS, direction: DESC}) { 166 | totalCount 167 | edges { 168 | node { 169 | name 170 | description 171 | languages(first: 1, orderBy: {field: SIZE, direction: DESC}) { 172 | edges { 173 | node { 174 | name 175 | } 176 | } 177 | } 178 | updatedAt 179 | forkCount 180 | stargazers { 181 | totalCount 182 | } 183 | } 184 | } 185 | } 186 | } 187 | } 188 | `)(Repo) -------------------------------------------------------------------------------- /src/components/Results.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { graphql } from 'react-apollo' 4 | import gql from 'graphql-tag' 5 | 6 | const Results = (props) => { 7 | const data = props && props.data && props.data.search ? props.data.search.edges : []; 8 | const searchList = data.map(repo => { 9 | return ( 10 | 11 | { repo.node.nameWithOwner } 12 | 13 | ) 14 | }); 15 | return ( 16 | 17 | { searchList } 18 | 19 | ); 20 | 21 | } 22 | 23 | const ResultList = styled.ul` 24 | position: absolute; 25 | top: 38px; 26 | left: 15px; 27 | background-color: #24292e; 28 | border-radius: 3px; 29 | ` 30 | 31 | const Link = styled.a` 32 | color: #fff; 33 | ` 34 | 35 | const SearchItem = styled.li` 36 | padding: 10px; 37 | font-size: 12px; 38 | cursor: pointer; 39 | &:hover { 40 | color: #24292e; 41 | background-color: #fff; 42 | } 43 | ` 44 | 45 | const ResultsWithQuery = graphql(gql` 46 | query githubSearch($query: String!) { 47 | search(query: $query, type: REPOSITORY, first: 10) { 48 | repositoryCount 49 | edges { 50 | node { 51 | ... on Repository { 52 | nameWithOwner 53 | stargazers { 54 | totalCount 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | `, {skip: (ownProps) => !ownProps.query})(Results); 62 | 63 | 64 | export default ResultsWithQuery; -------------------------------------------------------------------------------- /src/components/Search.js: -------------------------------------------------------------------------------- 1 | import React, { Component} from 'react' 2 | import ReactDOM from 'react-dom' 3 | import styled from 'styled-components' 4 | import ResultsWithQuery from './Results' 5 | 6 | export class Search extends Component { 7 | 8 | constructor(props) { 9 | super(props) 10 | this.state = { 11 | search: '', 12 | searchVisible: false 13 | } 14 | } 15 | 16 | componentDidMount() { 17 | document.addEventListener('click', this.handleClickOutside.bind(this), true) 18 | } 19 | 20 | componentWillUnmount() { 21 | document.removeEventListener('click', this.handleClickOutside.bind(this), true) 22 | } 23 | 24 | handleClickOutside(e) { 25 | 26 | const domNode = ReactDOM.findDOMNode(this); 27 | 28 | if (!domNode || !domNode.contains(e.target)) { 29 | this.setState({ 30 | searchVisible: false 31 | }); 32 | } 33 | } 34 | 35 | updateSearch = (e) => { 36 | this.setState({ 37 | search: e.target.value, 38 | searchVisible: true 39 | }) 40 | } 41 | 42 | setSearchVisible = () => { 43 | this.setState({ 44 | searchVisible: true 45 | }) 46 | } 47 | 48 | render() { 49 | 50 | const { search, searchVisible } = this.state; 51 | 52 | return ( 53 | 54 |
55 | 62 | { searchVisible && ( 63 | 64 | )} 65 | 66 |
67 | ) 68 | } 69 | } 70 | 71 | const SearchContainer = styled.div` 72 | position: relative; 73 | ` 74 | 75 | const SearchBar = styled.input` 76 | background: rgb(64, 68, 72); 77 | padding: 6px 8px; 78 | border-radius: 3px; 79 | width: 300px; 80 | border: none; 81 | margin-left: 15px; 82 | font-size: 16px; 83 | color: #fff; 84 | font-size: 12px; 85 | 86 | &:focus { 87 | outline: none; 88 | } 89 | ` 90 | 91 | export default Search 92 | -------------------------------------------------------------------------------- /src/components/Stars.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { graphql } from 'react-apollo' 3 | import gql from 'graphql-tag' 4 | import styled from 'styled-components' 5 | import moment from 'moment' 6 | import LoadingIndicator from './LoadingIndicator' 7 | 8 | class Stars extends Component { 9 | 10 | state = { 11 | starredRepositories: [], 12 | filteredRepos: [], 13 | filtered: false 14 | } 15 | 16 | componentDidMount() { 17 | if(this.props.data && this.props.data.viewer) { 18 | this.setState({ 19 | starredRepositories: this.props.data.viewer.starredRepositories.nodes 20 | }) 21 | } 22 | } 23 | 24 | componentWillReceiveProps(nextProps) { 25 | if(nextProps.data.viewer.starredRepositories) { 26 | this.setState({ 27 | starredRepositories: nextProps.data.viewer.starredRepositories.nodes 28 | }) 29 | } 30 | } 31 | 32 | searchRepos = (e) => { 33 | 34 | const repos = this.state.starredRepositories.filter(repo => { 35 | if(repo.name.indexOf(e.target.value) > -1) { 36 | return repo 37 | } else { 38 | return null 39 | } 40 | }) 41 | 42 | this.setState({ 43 | filteredRepos: repos, 44 | filtered: true 45 | }) 46 | } 47 | 48 | render() { 49 | 50 | const { starredRepositories, filteredRepos, filtered } = this.state; 51 | 52 | const visibleRepos = filtered ? filteredRepos : starredRepositories 53 | 54 | const repositories = starredRepositories.length > 0 ? visibleRepos.map((star, i) => { 55 | 56 | return ( 57 | 58 | 59 | { star.owner.login } / { star.name } 60 | 61 | { star.description } 62 | 63 | 64 | { star.languages.edges[0] && star.languages.edges[0].node && star.languages.edges[0].node.name ? star.languages.edges[0].node.name : null } 65 | { star.stargazers.totalCount.toLocaleString() } 66 | { star.forkCount.toLocaleString() } 67 | { moment(star.updatedAt).fromNow()} 68 | 69 | 70 | ) 71 | }) : 72 | 73 | return ( 74 |
75 | { starredRepositories.length > 0 76 | && ( 77 | 78 | 83 | 84 | 85 | ) 86 | } 87 | { repositories } 88 |
89 | ) 90 | } 91 | 92 | } 93 | 94 | const StarCard = styled.div` 95 | border-bottom: 1px #d1d5da solid; 96 | padding: 16px; 97 | margin-bottom: 16px; 98 | ` 99 | 100 | const StarDescription = styled.p` 101 | font-size: 14px; 102 | color: #586069; 103 | margin: 4px 0 8px 0; 104 | ` 105 | 106 | const SearchContainer = styled.div` 107 | border-bottom: 1px solid #d1d5da; 108 | padding-bottom: 16px; 109 | ` 110 | 111 | const SearchBox = styled.input` 112 | min-height: 34px; 113 | width: 300px; 114 | font-size: 14px; 115 | padding: 6px 8px; 116 | background-color: #fff; 117 | background-repeat: no-repeat; 118 | background-position: right 8px center; 119 | border: 1px solid #d1d5da; 120 | border-radius: 3px; 121 | outline: none; 122 | box-shadow: inset 0 1px 2px rgba(27,31,35,0.075); 123 | ` 124 | 125 | const Language = styled.span` 126 | margin-right: 10px; 127 | ` 128 | 129 | const InfoContainer = styled.div` 130 | display: flex; 131 | align-items: center; 132 | color: #586069; 133 | font-size: 12px; 134 | ` 135 | 136 | const Icon = styled.i` 137 | margin-right: 3px; 138 | color: #586069; 139 | ` 140 | 141 | const Date = styled.p` 142 | font-size: 12px; 143 | color: #586069; 144 | margin-bottom: 0; 145 | ` 146 | 147 | const Count = styled.p` 148 | font-size: 12px; 149 | color: #586069; 150 | margin-right: 12px; 151 | margin-bottom: 0; 152 | ` 153 | 154 | const Name = styled.span` 155 | font-size: 20px; 156 | ` 157 | 158 | const Owner = styled.span` 159 | font-weight: 600; 160 | font-size: 20px; 161 | ` 162 | 163 | const Circle = styled.div` 164 | height: 12px; 165 | width: 12px; 166 | border-radius: 50%; 167 | background: #f1e05a; 168 | margin-right: 5px; 169 | top: 2px; 170 | position: relative; 171 | ` 172 | 173 | const Link = styled.a` 174 | color: #0566D9; 175 | ` 176 | 177 | export default graphql(gql` 178 | { 179 | viewer { 180 | starredRepositories(first: 100) { 181 | totalCount 182 | nodes { 183 | name 184 | nameWithOwner 185 | description 186 | forkCount 187 | updatedAt 188 | languages(first: 1, orderBy: {field: SIZE, direction: DESC}) { 189 | edges { 190 | node { 191 | name 192 | } 193 | } 194 | } 195 | owner { 196 | login 197 | } 198 | stargazers { 199 | totalCount 200 | } 201 | } 202 | } 203 | } 204 | } 205 | `)(Stars) 206 | -------------------------------------------------------------------------------- /src/components/UserMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { NavLink } from 'react-router-dom' 4 | import { withRouter } from 'react-router-dom' 5 | 6 | const UserMenu = ({ username, id, closeMenu }) => ( 7 | 8 | 9 | {`Signed in as ${ username }`} 10 | 11 | 12 | 13 | Your Profile 14 | 15 | 16 | 17 | Your Followers 18 | 19 | 20 | 21 | Your Stars 22 | 23 | 24 | 25 | Help 26 | 27 | Settings 28 | Sign Out 29 | 30 | 31 | ) 32 | 33 | const UserMenuContainer = styled.ul` 34 | position: absolute; 35 | top: 100%; 36 | left: 0; 37 | z-index: 100; 38 | width: 160px; 39 | padding-top: 5px; 40 | padding-bottom: 5px; 41 | list-style: none; 42 | background-color: #fff; 43 | background-clip: padding-box; 44 | border: 1px solid rgba(27,31,35,0.15); 45 | border-radius: 4px; 46 | box-shadow: 0 3px 12px rgba(27,31,35,0.15); 47 | width: 180px; 48 | margin-top: 8px; 49 | ` 50 | 51 | const DropDownItem = styled.li` 52 | cursor: pointer; 53 | display: block; 54 | padding: 4px 10px 4px 15px; 55 | overflow: hidden; 56 | color: #24292e; 57 | text-overflow: ellipsis; 58 | white-space: nowrap; 59 | &:hover { 60 | color: #fff; 61 | background-color: #0366d6; 62 | } 63 | ` 64 | 65 | const DropDownDivider = styled.li` 66 | height: 1px; 67 | margin: 8px 1px; 68 | background-color: #e1e4e8; 69 | ` 70 | 71 | export default withRouter(UserMenu) 72 | -------------------------------------------------------------------------------- /src/images/octocat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | * { 51 | box-sizing: border-box; 52 | } 53 | 54 | body { 55 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; 56 | color: #24292e; 57 | line-height: 1.5; 58 | background-color: #fff; 59 | } 60 | 61 | a { 62 | text-decoration: none; 63 | } 64 | 65 | .float-left.text-gray, 66 | .contrib-column { 67 | display: none; 68 | } 69 | 70 | .calendar h2 { 71 | position: absolute; 72 | top: -30px; 73 | color: #24292e; 74 | } 75 | 76 | .calendar { 77 | min-height: 100px; 78 | width: 100%; 79 | margin: 30px 0 0 0; 80 | border: none; 81 | } 82 | 83 | .calendar-graph { 84 | padding: 15px 0 0; 85 | } 86 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | import { BrowserRouter } from 'react-router-dom' 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , document.getElementById('root') 11 | ) --------------------------------------------------------------------------------