├── .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 | 
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 |
66 |
67 |
68 |
69 |
70 | }/>
79 |
80 |
81 |
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 | FollowersInfoContainer>
27 |
28 |
29 |
30 | )
31 | }) :
32 |
33 | return (
34 |
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 | FollowersInfoContainer>
32 |
33 |
34 |
35 | )
36 | }) :
37 |
38 | return (
39 |
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 |
13 |
14 | { issue.node.repository.nameWithOwner } { issue.node.title }
15 | opened on {`${ moment(issue.node.publishedAt).format("ddd MMM YYYY") }`} by {`${ issue.node.author.login }`}
16 |
17 |
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 |
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 } { repo.node.stargazers.totalCount } { repo.node.forkCount }
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 | { company }
29 |
30 | )}
31 | {location && (
32 |
33 | { location }
34 |
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 |
13 |
14 | { pr.node.repository.nameWithOwner } { pr.node.title }
15 | opened on {`${ moment(pr.node.publishedAt).format("ddd MMM YYYY") }`} by {`${ pr.node.author.login }`}
16 |
17 |
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 |
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 | )
--------------------------------------------------------------------------------