├── .env
├── .github
└── FUNDING.yml
├── .gitignore
├── LICENSE
├── README.md
├── babel-server.config.js
├── github.gif
├── home.png
├── package.json
├── promo
└── gitix.png
├── promotion
└── icons
│ └── ic_launcher_gitix_512.png
├── public
├── _headers
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── images
│ ├── bitbucket.png
│ ├── bitbucket.svg
│ ├── git.png
│ ├── git.svg
│ ├── github.png
│ ├── github.svg
│ ├── githuman.png
│ ├── gitix.png
│ ├── gitlab.png
│ ├── gitlab.svg
│ ├── menu.svg
│ ├── octocat.svg
│ └── user.png
├── index.html
├── manifest.json
├── mstile-150x150.png
└── safari-pinned-tab.svg
├── server
└── index.js
├── src
├── App.js
├── components
│ ├── AddRepositories.js
│ ├── App-Container.js
│ ├── Avatar.js
│ ├── Contributions.js
│ ├── CreateRepo.js
│ ├── Followers.css
│ ├── Followers.js
│ ├── Following.js
│ ├── Footer.js
│ ├── Issues.js
│ ├── LoadingChecker.js
│ ├── LoadingIndicator.js
│ ├── LoginScreen.js
│ ├── LoginView.js
│ ├── Nav.js
│ ├── Overview.js
│ ├── Profile.js
│ ├── ProfileDetails.js
│ ├── ProfileMenu.js
│ ├── PullRequests.js
│ ├── RepoCard.js
│ ├── Repositories.js
│ ├── Stars.js
│ ├── User.js
│ ├── UserMenu.js
│ ├── constants.js
│ └── models.js
├── images
│ └── octocat.svg
├── index.css
├── index.js
└── lib
│ └── blockstack.js
└── yarn.lock
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_VERSION=$npm_package_version
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | custom: ['https://opencollective.com/openintents/contribute']
4 |
--------------------------------------------------------------------------------
/.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 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/babel-server.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | "@babel/preset-react",
4 | "@babel/preset-env",
5 | ],
6 | plugins: [
7 | "@babel/plugin-proposal-class-properties",
8 | ],
9 | };
10 |
--------------------------------------------------------------------------------
/github.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openintents/gitix/d18da4127170963c2f7ea707852d4c2cdd005320/github.gif
--------------------------------------------------------------------------------
/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openintents/gitix/d18da4127170963c2f7ea707852d4c2cdd005320/home.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gitix",
3 | "version": "0.3.0",
4 | "homepage": "/",
5 | "dependencies": {
6 | "@babel/node": "^7.7.4",
7 | "@babel/plugin-proposal-class-properties": "^7.7.4",
8 | "@babel/preset-env": "^7.7.4",
9 | "@babel/preset-react": "^7.7.4",
10 | "@material-ui/core": "^4.5.2",
11 | "@material-ui/icons": "^4.5.1",
12 | "blockstack": "^20.0.0-alpha.5",
13 | "blockstack-collections": "0.1.8",
14 | "cors": "^2.8.5",
15 | "express-ws": "^4.0.0",
16 | "github-calendar": "^1.3.2",
17 | "gitstar-components": "^1.0.5",
18 | "lodash": "^4.17.15",
19 | "moment": "^2.21.0",
20 | "radiks": "^0.3.0-beta.1",
21 | "radiks-server": "^0.3.0-beta.1",
22 | "react": "^16.11.0",
23 | "react-blockstack": "^0.6.3",
24 | "react-blockstack-button": "^0.1.0",
25 | "react-dom": "^16.11.0",
26 | "react-helmet": "^5.2.1",
27 | "react-router": "^5.1.2",
28 | "react-router-dom": "^5.1.2",
29 | "react-scripts": "3.2.0",
30 | "react-share": "^3.0.1",
31 | "styled-components": "^4.4.1"
32 | },
33 | "scripts": {
34 | "babel-node": "babel-node --config-file ./babel-server.config.js",
35 | "start-web": "react-scripts start",
36 | "build": "react-scripts build",
37 | "test": "react-scripts test --env=jsdom",
38 | "eject": "react-scripts eject",
39 | "start": "NODE_ENV=production yarn babel-node server"
40 | },
41 | "browserslist": [
42 | ">0.2%",
43 | "not dead",
44 | "not ie <= 11",
45 | "not op_mini all"
46 | ]
47 | }
48 |
--------------------------------------------------------------------------------
/promo/gitix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openintents/gitix/d18da4127170963c2f7ea707852d4c2cdd005320/promo/gitix.png
--------------------------------------------------------------------------------
/promotion/icons/ic_launcher_gitix_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openintents/gitix/d18da4127170963c2f7ea707852d4c2cdd005320/promotion/icons/ic_launcher_gitix_512.png
--------------------------------------------------------------------------------
/public/_headers:
--------------------------------------------------------------------------------
1 | /index.html
2 | Access-Control-Allow-Origin: *
3 | Access-Control-Allow-Headers: "X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token, Accept-Encoding"
4 | Access-Control-Allow-Methods: "POST, GET, OPTIONS, DELETE, PUT"
5 | /manifest.json
6 | Access-Control-Allow-Origin: *
7 | Access-Control-Allow-Headers: "X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token, Accept-Encoding"
8 | Access-Control-Allow-Methods: "POST, GET, OPTIONS, DELETE, PUT"
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openintents/gitix/d18da4127170963c2f7ea707852d4c2cdd005320/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openintents/gitix/d18da4127170963c2f7ea707852d4c2cdd005320/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openintents/gitix/d18da4127170963c2f7ea707852d4c2cdd005320/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openintents/gitix/d18da4127170963c2f7ea707852d4c2cdd005320/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openintents/gitix/d18da4127170963c2f7ea707852d4c2cdd005320/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openintents/gitix/d18da4127170963c2f7ea707852d4c2cdd005320/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/bitbucket.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openintents/gitix/d18da4127170963c2f7ea707852d4c2cdd005320/public/images/bitbucket.png
--------------------------------------------------------------------------------
/public/images/bitbucket.svg:
--------------------------------------------------------------------------------
1 |
2 |
106 |
--------------------------------------------------------------------------------
/public/images/git.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openintents/gitix/d18da4127170963c2f7ea707852d4c2cdd005320/public/images/git.png
--------------------------------------------------------------------------------
/public/images/git.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
16 |
--------------------------------------------------------------------------------
/public/images/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openintents/gitix/d18da4127170963c2f7ea707852d4c2cdd005320/public/images/github.png
--------------------------------------------------------------------------------
/public/images/github.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/images/githuman.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openintents/gitix/d18da4127170963c2f7ea707852d4c2cdd005320/public/images/githuman.png
--------------------------------------------------------------------------------
/public/images/gitix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openintents/gitix/d18da4127170963c2f7ea707852d4c2cdd005320/public/images/gitix.png
--------------------------------------------------------------------------------
/public/images/gitlab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openintents/gitix/d18da4127170963c2f7ea707852d4c2cdd005320/public/images/gitlab.png
--------------------------------------------------------------------------------
/public/images/gitlab.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/images/menu.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/public/images/octocat.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openintents/gitix/d18da4127170963c2f7ea707852d4c2cdd005320/public/images/user.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
26 |
30 |
31 |
32 | gitix.org | Decentralized git profiles
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
93 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Gitix",
3 | "name": "Gitix | Decentralized home of all git contributions",
4 | "icons": [
5 | {
6 | "src": "https://app.gitix.org/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 | "did_authors": ["did:stack:v0:1E72AfQC47JaR3DRhwWVaRnguppAsKRCk4-0", "did:stack:v0:1Mk9gNKVdeLsodtbtUssVJmJMRHaEa2hGF-67"]
16 | }
17 |
--------------------------------------------------------------------------------
/public/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openintents/gitix/d18da4127170963c2f7ea707852d4c2cdd005320/public/mstile-150x150.png
--------------------------------------------------------------------------------
/public/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
46 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const cors = require('cors')
3 | const expressWS = require('express-ws');
4 | const { setup } = require('radiks-server');
5 |
6 | const app = express();
7 | expressWS(app)
8 |
9 | app.use(cors())
10 |
11 | var server = require('http').Server(app),
12 |
13 | port = process.env.PORT || 5000;
14 | server.listen(port);
15 |
16 | console.log("listening to port ", port);
17 |
18 |
19 | setup({
20 | mongoDBUrl: process.env.MONGODB_URI || 'mongodb://localhost:27017/radiks-server'
21 | }).then((RadiksController) => {
22 | app.use('/radiks', RadiksController);
23 | app.use(express.static('public'))
24 | console.log("Radiks server live")
25 | });
26 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import AppContainer from "./components/App-Container";
3 | import { withRouter } from "react-router-dom";
4 | import { useBlockstack } from "react-blockstack";
5 | import { Helmet } from "react-helmet";
6 | import { configure, User } from "radiks";
7 | import { RADIKS_SERVER_URL } from "./components/constants";
8 | import Footer from "./components/Footer";
9 |
10 | const NotSignedIn = {
11 | checking: false,
12 | user: null,
13 | isSignedIn: false
14 | };
15 |
16 | const App = () => {
17 | const [ , setSignIn] = useState({ ...NotSignedIn, checking: true });
18 | const { userData, userSession } = useBlockstack();
19 | useEffect(() => {
20 | configure({
21 | apiServer: RADIKS_SERVER_URL,
22 | userSession
23 | });
24 | }, [userSession]);
25 |
26 | useEffect(() => {
27 | if (userData) {
28 | console.log("user configured")
29 | User.createWithCurrentUser().then(() => {
30 | setSignIn({ checking: false, user: userData, isSignedIn: true });
31 | });
32 | } else {
33 | setSignIn(NotSignedIn);
34 | }
35 | }, [userData]);
36 |
37 | const title = "gitix.org";
38 | const metaDescription = "Decentralized git profiles";
39 | const img = "https://app.gitix.org/favicon.ico";
40 | return (
41 | <>
42 |
43 | {title}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | >
58 | );
59 | };
60 |
61 | export default withRouter(App);
62 |
--------------------------------------------------------------------------------
/src/components/AddRepositories.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import styled from "styled-components";
3 | import LoadingIndicator from "./LoadingIndicator";
4 | import {
5 | putNewRepository,
6 | getGithubRepos,
7 | isUserSignedIn,
8 | loadUserData
9 | } from "../lib/blockstack";
10 |
11 | class AddRepo extends Component {
12 | state = {
13 | repos: [],
14 | loading: true,
15 | name: "",
16 | url: "",
17 | description: "",
18 | updating: false
19 | };
20 |
21 | componentDidMount() {
22 | if (isUserSignedIn()) {
23 | getGithubRepos(loadUserData().profile).then(repos => {
24 | console.log(repos);
25 | if (repos) {
26 | this.setState({
27 | repos
28 | });
29 | }
30 | this.setState({ loading: false });
31 | });
32 | }
33 | }
34 |
35 | handleChangeName(event) {
36 | this.setState({ name: event.target.value });
37 | }
38 |
39 | handleChangeUrl(event) {
40 | this.setState({ url: event.target.value });
41 | }
42 |
43 | handleChangeDescription(event) {
44 | this.setState({ description: event.target.value });
45 | }
46 |
47 | saveRepository() {
48 | this.setState({ updating: true });
49 | const newRepo = {
50 | name: this.state.name,
51 | url: this.state.url,
52 | description: this.state.description
53 | };
54 |
55 | putNewRepository(newRepo).then(r => {
56 | this.setState({ updating: false });
57 | window.location.href = window.location.origin;
58 | });
59 | }
60 |
61 | render() {
62 | const { loading, updating, repos } = this.state;
63 |
64 | return (
65 |
66 |
Add repository
67 | {repos.length > 0 && (
68 | <>
69 | Repo from your github acount:{" "}
70 |
98 | >
99 | )}
100 | Repository Details
101 |
102 | this.handleChangeName(e)}
105 | />
106 |
107 | this.handleChangeUrl(e)}
110 | />
111 |
112 | this.handleChangeDescription(e)}
115 | />
116 |
117 | this.saveRepository()}>
118 | Save
119 |
120 | {(loading || updating) && }
121 |
122 | );
123 | }
124 | }
125 |
126 | const SaveButton = styled.a`
127 | cursor: pointer;
128 | border-radius: 0.25em;
129 | color: white;
130 | background-color: #28a745;
131 | background-image: linear-gradient(-180deg, #34d058, #28a745 90%);
132 | font-size: 12px;
133 | line-height: 20px;
134 | padding: 3px 10px;
135 | background-position: -1px -1px;
136 | background-repeat: repeat-x;
137 | background-size: 110% 110%;
138 | border: 1px solid rgba(27, 31, 35, 0.2);
139 | display: inline-block;
140 | font-weight: 600;
141 | position: relative;
142 | vertical-align: middle;
143 | white-space: nowrap;
144 | text-decoration: none;
145 | box-sizing: border-box;
146 | margin: 8px 0px;
147 | &:hover: {
148 | text-decoration: none;
149 | }
150 | `;
151 |
152 | const Title = styled.p`
153 | color: #24292e;
154 | font-size: 16px;
155 | margin-bottom: 8px;
156 | `;
157 |
158 | const Label = styled.p`
159 | font-size: 12px;
160 | color: #586069;
161 | margin-left: 10px;
162 | margin-bottom: 0;
163 | `;
164 |
165 | const RepoDescription = styled.textarea`
166 | font-size: 14px;
167 | color: #586069;
168 | width: 460px;
169 | `;
170 |
171 | const RepoUrl = styled.input`
172 | color: #586069;
173 | font-size: 14px;
174 | width: 460px;
175 | `;
176 |
177 | const RepoName = styled.input`
178 | color: #586069;
179 | font-size: 14px;
180 | width: 460px;
181 | `;
182 |
183 | const ButtonIcon = styled.i``;
184 |
185 | export default AddRepo;
186 |
--------------------------------------------------------------------------------
/src/components/App-Container.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import styled from "styled-components";
3 | import { Route, Switch } from "react-router-dom";
4 | import { useBlockstack } from "react-blockstack";
5 |
6 | import Nav from "./Nav";
7 | import Profile from "./Profile";
8 | import Overview from "./Overview";
9 | import Repositories from "./Repositories";
10 | import AddRepositories from "./AddRepositories";
11 | import Followers from "./Followers";
12 | import Following from "./Following";
13 | import ProfileMenu from "./ProfileMenu";
14 | import PullRequests from "./PullRequests";
15 | import CreateRepo from "./CreateRepo";
16 | import Issues from "./Issues";
17 | import Stars from "./Stars";
18 | import User from "./User";
19 |
20 | import LoginScreen from "./LoginScreen";
21 |
22 | const Home = ({
23 | avatarUrl,
24 | userFullName,
25 | username,
26 | location,
27 | company,
28 | bio,
29 | organizations
30 | }) => {
31 | return (
32 |
33 |
42 |
43 |
44 |
45 |
46 |
51 |
56 |
61 |
65 |
69 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | const App = () => {
77 | const [state, setState] = useState({ viewer: null });
78 | const { userData } = useBlockstack();
79 | useEffect(() => {
80 | if (userData) {
81 | const user = userData;
82 | const avatarUrl =
83 | (user.profile &&
84 | user.profile.image &&
85 | user.profile.image.length > 0 &&
86 | user.profile.image[0].contentUrl) ||
87 | "/images/user.png";
88 | const username = user.username || user.identityAddress;
89 | const userFullName = user.profile && user.profile.name;
90 | const bio = user.profile && user.profile.description;
91 | setState({
92 | viewer: {
93 | avatarUrl,
94 | userFullName,
95 | username,
96 | location: null,
97 | company: null,
98 | bio,
99 | organizations: {}
100 | }
101 | });
102 | }
103 | }, [userData]);
104 |
105 | const { viewer } = state;
106 | const avatarUrl = viewer ? viewer.avatarUrl : "";
107 | const userFullName = viewer ? viewer.userFullName : "";
108 | const username = viewer ? viewer.username : "";
109 | const location = viewer ? viewer.location : "";
110 | const company = viewer ? viewer.company : "";
111 | const bio = viewer ? viewer.bio : "";
112 | const organizations = viewer ? viewer.organizations : {};
113 |
114 | return (
115 |
116 |
117 |
118 |
122 |
123 |
127 |
131 |
136 | (
139 | <>
140 | {viewer && (
141 |
150 | )}
151 | {!userData && }
152 | >
153 | )}
154 | />
155 |
156 |
157 | );
158 | };
159 |
160 | const ProfileContainer = styled.section`
161 | max-width: 1012px;
162 | margin: 0 auto;
163 | display: block;
164 | @media (min-width: 768px) {
165 | display: flex;
166 | }
167 | `;
168 |
169 | const InformationContainer = styled.section`
170 | margin-top: 24px;
171 | padding: 0px 20px;
172 | `;
173 |
174 | export default App;
175 |
--------------------------------------------------------------------------------
/src/components/Avatar.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { isUserSignedIn, loadUserData } from "blockstack";
4 |
5 | class UserAvatar extends React.Component {
6 | state = { loading: true, error: null, data: null };
7 |
8 | componentDidMount() {
9 | if (isUserSignedIn()) {
10 | const user = loadUserData();
11 | const avatarUrl =
12 | (user.profile &&
13 | user.profile.image &&
14 | user.profile.image.length > 0 &&
15 | user.profile.image[0].contentUrl) ||
16 | "/images/user.png";
17 | this.setState({
18 | loading: false,
19 | error: null,
20 | data: {
21 | viewer: {
22 | avatarUrl
23 | }
24 | }
25 | });
26 | }
27 | }
28 |
29 | render() {
30 | const { loading, error, data } = this.state;
31 | const { onClick } = this.props;
32 | if (loading) return Loading...
;
33 | if (error) return Error :(
;
34 |
35 | if (data.viewer && data.viewer.avatarUrl) {
36 | return ;
37 | } else {
38 | return ;
39 | }
40 | }
41 | }
42 |
43 | const ProfilePic = styled.img`
44 | border-radius: 3px;
45 | height: 20px;
46 | width: 20px;
47 | cursor: pointer;
48 | margin-right: 4px;
49 | margin-top: 8px;
50 | `;
51 |
52 | const Placeholder = styled.img`
53 | border-radius: 3px;
54 | height: 20px;
55 | width: 20px;
56 | cursor: pointer;
57 | margin-right: 4px;
58 | margin-top: 8px;
59 | background: #888;
60 | `;
61 |
62 | export default UserAvatar;
63 |
--------------------------------------------------------------------------------
/src/components/Contributions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components'
3 |
4 | const Contributions = () => {
5 | const [repositories, setRepositories] = useState([])
6 | const repos = repositories ? repositories.map(repo => (
7 |
8 | { repo.name }
9 | { repo.description }
10 | { repo.languages[0].name } { repo.stargazers.totalCount } { repo.forkCount }
11 |
12 | )
13 | ) : []
14 |
15 | return (
16 |
17 | { repos }
18 |
19 | )
20 | }
21 |
22 | const RepoContainer = styled.div`
23 | display: flex;
24 | flex-wrap: wrap;
25 | justify-content: space-between;
26 | `
27 |
28 | const RepoCard = styled.div`
29 | border: 1px #d1d5da solid;
30 | padding: 16px;
31 | width: 362px;
32 | margin-bottom: 16px;
33 | `
34 |
35 | const RepoDescription = styled.p`
36 | font-size: 12px;
37 | color: #586069;
38 | `
39 |
40 | const RepoLink = styled.a`
41 | font-weight: 600;
42 | font-size: 14px;
43 | color: #0366d6;
44 | `
45 |
46 | const RepoDetails = styled.span`
47 | color: #586069;
48 | font-size: 12px;
49 | `
50 |
51 | const Icon = styled.i`
52 | margin-left: 16px;
53 | `
54 |
55 | export default Contributions
56 |
--------------------------------------------------------------------------------
/src/components/CreateRepo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | const CreateRepo = () => {
5 | return (
6 |
7 |
8 |
9 | Gitix does not provide hosting for git repositories. Please decide for
10 | yourself which hosting solution is the best for you.
11 |
12 | Here are some suggestions:
13 |
14 |
15 |
16 |
17 | Githuman
18 | {" "}
19 | - decentralized, using IPFS
20 |
21 |
22 |
27 |
28 | Host yourself
29 | {" "}
30 | - Read the instructions
31 |
32 |
33 |
38 |
39 | Gitlab
40 | {" "}
41 | - A single application for the entire DevOps lifecycle
42 |
43 |
44 |
45 |
46 | Github
47 | {" "}
48 | - Built for developers
49 |
50 |
51 |
52 |
53 | Bitbucket
54 | {" "}
55 | - Built for professional teams
56 |
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | const CreateRepoContainer = styled.section`
64 | width: 980px;
65 | margin: 0 auto;
66 | `;
67 |
68 | const ProviderList = styled.ul`
69 | font-size: 14px;
70 | :last-child {
71 | margin-left: 10px;
72 | }
73 | `;
74 |
75 | const ProviderItem = styled.li``;
76 |
77 | const Link = styled.a`
78 | text-decoration: none;
79 | `;
80 |
81 | const ProviderIcon = styled.img`
82 | width: 16px;
83 | height: 16px;
84 | display: inline-block;
85 | vertical-align: middle;
86 | margin-right: 10px;
87 | `;
88 |
89 | export default CreateRepo;
90 |
--------------------------------------------------------------------------------
/src/components/Followers.css:
--------------------------------------------------------------------------------
1 | .share {
2 | vertical-align: top;
3 | display: inline-block;
4 | margin-right: 15px;
5 | text-align: center;
6 | }
7 |
8 | .share-button {
9 | cursor: pointer;
10 | }
11 |
12 | .share-button:hover:not(:active) {
13 | opacity: 0.75;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/Followers.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import styled from "styled-components";
3 | import { Relation } from "./models";
4 | import { useBlockstack } from "react-blockstack";
5 | import { lookupProfile } from "../lib/blockstack";
6 | import {
7 | FacebookShareButton,
8 | FacebookIcon,
9 | TwitterShareButton,
10 | TwitterIcon,
11 | TelegramShareButton,
12 | TelegramIcon,
13 | RedditShareButton,
14 | RedditIcon,
15 | EmailShareButton,
16 | EmailIcon
17 | } from "react-share";
18 | import "./Followers.css";
19 |
20 | const Followers = () => {
21 | const [followerList, setFollowerList] = useState([]);
22 | const [loading, setLoading] = useState(false);
23 | const { userData } = useBlockstack();
24 |
25 | useEffect(() => {
26 | setLoading(true);
27 |
28 | Relation.fetchList({ followee: userData.username }).then(followers => {
29 | setFollowerList(followers);
30 | Promise.all(
31 | followers.map(f => {
32 | console.log(f);
33 | return lookupProfile(f.attrs.follower).then(p => {
34 | console.log(p);
35 | return {
36 | name: p.name,
37 | login: f.attrs.follower,
38 | avatarUrl: p.image[0].contentUrl,
39 | bio: p.description
40 | };
41 | });
42 | })
43 | ).then(profiles => {
44 | setFollowerList(profiles);
45 | setLoading(false);
46 | });
47 | });
48 | }, [userData.username]);
49 |
50 | const isAndroid = /(android)/i.test(navigator.userAgent);
51 | const title = "My Gitix Profile";
52 | const url = `https://app.gitix.org/#/u/${userData.username}`;
53 | const shareButtons = (
54 | <>
55 | {navigator && navigator.share && (
56 |
58 | navigator.share({
59 | title,
60 | url
61 | })
62 | }
63 | >
64 | Share your profile
65 |
66 | )}
67 | {navigator && !navigator.share && isAndroid && (
68 | {
70 | window.open(
71 | `intent://share/#Intent;action=android.intent.action.SEND;S.android.intent.extra.TEXT=${title}:${url};end`
72 | );
73 | }}
74 | >
75 | Share your profile
76 |
77 | )}
78 | {!navigator ||
79 | (!navigator.share && !isAndroid && (
80 | <>
81 | Share your profile
82 |
83 |
84 |
89 |
90 |
91 |
92 |
93 |
98 |
99 |
100 |
101 |
102 |
107 |
108 |
109 |
110 |
111 |
118 |
119 |
120 |
121 |
122 |
128 |
129 |
130 |
131 | >
132 | ))}
133 | >
134 | );
135 |
136 | const followers =
137 | followerList.length > 0 ? (
138 | followerList.map((follower, i) => {
139 | return (
140 |
141 |
142 |
143 |
144 |
145 | {follower.name}
146 | {follower.login}
147 |
148 | {follower.bio}
149 | {follower.location && (
150 |
151 |
152 | {follower.location}
153 |
154 | )}
155 |
156 |
157 |
158 | );
159 | })
160 | ) : (
161 |
162 | Share your profile with your friends, family, colleagues to follow you.
163 |
164 | Your repositories will appear in their overview page.
165 |
166 | );
167 | return (
168 | <>
169 |
170 | {followers}
171 | {loading && Loading...
}
172 |
173 | {shareButtons}
174 |
175 | >
176 | );
177 | };
178 |
179 | const Icon = styled.i`
180 | font-size: 18px;
181 | margin-left: 4px;
182 | `;
183 |
184 | const FollowersContainer = styled.div`
185 | display: flex;
186 | `;
187 |
188 | const FollowersInfoContainer = styled.div`
189 | font-size: 12px;
190 | `;
191 |
192 | const FollowersName = styled.div`
193 | display: flex;
194 | align-items: flex-end;
195 | margin-bottom: 4px;
196 | `;
197 |
198 | const FollowersImage = styled.img`
199 | height: 50px;
200 | width: 50px;
201 | border-radius: 3px;
202 | margin-right: 5px;
203 | `;
204 |
205 | const FollowersCard = styled.div`
206 | border-bottom: 1px #d1d5da solid;
207 | padding: 16px;
208 | margin-bottom: 16px;
209 | `;
210 |
211 | const FollowerName = styled.p`
212 | font-size: 16px;
213 | color: #24292e;
214 | padding-left: 4px;
215 | margin-bottom: 0;
216 | `;
217 |
218 | const FollowerLogin = styled.p`
219 | font-size: 14px;
220 | color: #586069;
221 | padding-left: 4px;
222 | position: relative;
223 | margin-bottom: 0;
224 | top: -1px;
225 | `;
226 |
227 | const FollowerLocation = styled.p`
228 | font-size: 14px;
229 | color: #586069;
230 | padding-left: 4px;
231 | display: inline-block;
232 | margin-bottom: 4px;
233 | `;
234 |
235 | const FollowerBio = styled.p`
236 | font-size: 14px;
237 | color: #586069;
238 | padding-left: 4px;
239 | margin-bottom: 4px;
240 | `;
241 |
242 | const ShareButton = styled.a`
243 | cursor: pointer;
244 | border-radius: 0.25em;
245 | color: white;
246 | background-color: #28a745;
247 | background-image: linear-gradient(-180deg, #34d058, #28a745 90%);
248 | font-size: 12px;
249 | line-height: 20px;
250 | padding: 3px 10px;
251 | background-position: -1px -1px;
252 | background-repeat: repeat-x;
253 | background-size: 110% 110%;
254 | border: 1px solid rgba(27, 31, 35, 0.2);
255 | display: inline-block;
256 | font-weight: 600;
257 | position: relative;
258 | vertical-align: middle;
259 | white-space: nowrap;
260 | text-decoration: none;
261 | box-sizing: border-box;
262 | margin: 8px 0px;
263 | &:hover: {
264 | text-decoration: none;
265 | }
266 | `;
267 |
268 | export default Followers;
269 |
--------------------------------------------------------------------------------
/src/components/Following.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import styled from "styled-components";
3 | import LoadingIndicator from "./LoadingIndicator";
4 | import { getFile, loadUserData } from "blockstack";
5 | import { NavLink } from "react-router-dom";
6 |
7 | const suggestedUsers = [
8 | {
9 | avatarUrl:
10 | "https://gaia.blockstack.org/hub/1Maw8BjWgj6MWrBCfupqQuWANthMhefb2v/0/avatar-0",
11 | name: "Friedger Müffke",
12 | username: "friedger.id",
13 | bio: "Entredeveloper in Europe"
14 | },
15 | {
16 | avatarUrl:
17 | "https://gaia.blockstack.org/hub/1fHF3QADKT62js8BqHXmF31CFA1KUcTud/0//avatar-0",
18 | name: "Larry Salibra",
19 | username: "larry.id",
20 | bio: "Building a new internet for decentralized apps!"
21 | },
22 | {
23 | avatarUrl:
24 | "https://gaia.blockstack.org/hub/19AAYhqyS33YH8zrdmyN1PmEez8ZEEPQdT/0/avatar-0",
25 | name: "Dan Trevino",
26 | username: "dantrevino.id",
27 | bio: "Life, Liberty, and the Pursuit of Open Standards"
28 | }
29 | ];
30 |
31 | export const toFollowingUser = (follower, i) => {
32 | return (
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | {follower.name}
41 | {follower.username}
42 |
43 |
44 | {follower.bio}
45 | {follower.location && (
46 |
47 |
48 | {follower.location}
49 |
50 | )}
51 |
52 |
53 |
54 | );
55 | };
56 |
57 | const Following = () => {
58 | const [loading, setLoading] = useState(false);
59 | const [following, setFollowing] = useState([]);
60 | const [suggestedFollowers, setSuggestedFollowers] = useState(suggestedUsers);
61 | useEffect(() => {
62 | setLoading(true);
63 | const name = loadUserData().username || "";
64 |
65 | getFile("following").then(f => {
66 | let followingUsers;
67 | if (f) {
68 | followingUsers = JSON.parse(f);
69 | } else {
70 | followingUsers = [];
71 | }
72 | setFollowing(followingUsers);
73 |
74 | const gitixSuggestions = suggestedUsers.filter(
75 | u =>
76 | u.username !== name &&
77 | followingUsers.filter(fu => fu.username === u.username).length === 0
78 | );
79 | setSuggestedFollowers(gitixSuggestions);
80 |
81 | setLoading(false);
82 | });
83 | }, []);
84 |
85 | const follow = !loading ? (
86 | following.map(toFollowingUser)
87 | ) : (
88 |
89 | );
90 |
91 | return (
92 | <>
93 |
94 | {suggestedFollowers.length > 0 && (
95 | <>
96 | Users you might want to follow
97 | {suggestedFollowers.map(toFollowingUser)}
98 | >
99 | )}
100 | >
101 | );
102 | };
103 |
104 | const Title = styled.p`
105 | color: #24292e;
106 | font-size: 16px;
107 | margin-bottom: 8px;
108 | `;
109 |
110 | const Icon = styled.i`
111 | font-size: 18px;
112 | margin-left: 4px;
113 | `;
114 |
115 | const FollowersContainer = styled.div`
116 | display: flex;
117 | `;
118 |
119 | const FollowersInfoContainer = styled.div`
120 | font-size: 12px;
121 | `;
122 |
123 | const FollowersName = styled.div`
124 | display: flex;
125 | align-items: flex-end;
126 | margin-bottom: 4px;
127 | `;
128 |
129 | const FollowersImage = styled.img`
130 | height: 50px;
131 | width: 50px;
132 | border-radius: 3px;
133 | margin-right: 5px;
134 | `;
135 |
136 | const FollowersCard = styled.div`
137 | border-bottom: 1px #d1d5da solid;
138 | padding: 16px;
139 | margin-bottom: 16px;
140 | `;
141 |
142 | const FollowerName = styled.p`
143 | font-size: 16px;
144 | color: #24292e;
145 | padding-left: 4px;
146 | margin-bottom: 0;
147 | `;
148 |
149 | const FollowerLogin = styled.p`
150 | font-size: 14px;
151 | margin-bottom: 0;
152 | color: #586069;
153 | padding-left: 4px;
154 | position: relative;
155 | top: -1px;
156 | `;
157 |
158 | const FollowerLocation = styled.p`
159 | font-size: 14px;
160 | color: #586069;
161 | padding-left: 4px;
162 | display: inline-block;
163 | margin-bottom: 4px;
164 | `;
165 |
166 | const FollowerBio = styled.p`
167 | font-size: 14px;
168 | color: #586069;
169 | padding-left: 4px;
170 | margin-bottom: 4px;
171 | `;
172 |
173 | export default Following;
174 |
--------------------------------------------------------------------------------
/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Container, Grid, Typography } from "@material-ui/core";
3 | import AboutIcon from "@material-ui/icons/Info";
4 | import SupportIcon from "@material-ui/icons/Help";
5 | import DonateIcon from "@material-ui/icons/Stars";
6 | import { RADIKS_SERVER_URL } from "./constants";
7 | const styles = {
8 | tinyIcon: {
9 | width: 12,
10 | height: 12
11 | }
12 | };
13 | export default () => (
14 |
15 |
16 |
17 |
18 |
19 | About
20 |
21 |
26 | OpenIntents
27 |
28 |
29 |
34 | Source Code
35 |
36 |
37 |
38 | Terms
39 |
40 |
41 |
42 | Built with
43 |
44 |
49 | Blockstack
50 |
51 |
© {new Date().getFullYear()}, OpenIntents
52 |
Version {process.env.REACT_APP_VERSION}
53 |
Radiks Server: {RADIKS_SERVER_URL}
54 |
55 |
56 |
57 |
58 | Support
59 |
60 |
65 | ProductHunt
66 |
67 |
68 |
73 | Dmail
74 |
75 |
76 |
81 | Twitter
82 |
83 |
84 |
89 | Known Issues
90 |
91 |
92 |
93 |
94 |
95 | Love OI apps?
96 |
97 |
102 | Open Collective OpenIntents
103 |
104 |
105 |
110 | BitPatron
111 |
112 |
113 |
114 |
115 |
116 | );
117 |
--------------------------------------------------------------------------------
/src/components/Issues.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import styled from "styled-components";
3 | import moment from "moment";
4 |
5 | const Issues = () => {
6 | const [issueList] = useState([]);
7 |
8 | const issues = issueList
9 | ? issueList.map(issue => (
10 |
11 |
12 |
13 | {issue.repository.nameWithOwner}{" "}
14 | {issue.node.title}
15 |
16 | opened on {`${moment(issue.publishedAt).format("ddd MMM YYYY")}`}{" "}
17 | by {`${issue.author.login}`}
18 |
19 |
20 |
21 | ))
22 | : [];
23 |
24 | const openIssues = issueList
25 | ? issueList.filter(issue => {
26 | return issue.state === "OPEN";
27 | }).length
28 | : null;
29 |
30 | const closedIssues = issueList
31 | ? issueList.filter(issue => {
32 | return issue.state === "CLOSED";
33 | }).length
34 | : null;
35 |
36 | return (
37 |
38 |
39 | Issues across all git repos will be shown here in the future
40 | {openIssues ? `${openIssues} Open` : null}
41 |
42 | {closedIssues ? `${closedIssues} Closed` : null}
43 |
44 |
45 | {issues}
46 |
47 | );
48 | };
49 |
50 | const IssueContainer = styled.section`
51 | width: 980px;
52 | margin: 0 auto;
53 | border-left: 1px solid #e1e4e8;
54 | border-right: 1px solid #e1e4e8;
55 | border-top: 1px solid #e1e4e8;
56 | `;
57 |
58 | const IssueCountBG = styled.div`
59 | width: 980px;
60 | margin: 0 auto;
61 | background: #f6f8fa;
62 | border-top: 1px solid #e1e4e8;
63 | border-left: 1px solid #e1e4e8;
64 | border-right: 1px solid #e1e4e8;
65 | border-radius: 3px 3px 0 0;
66 | padding-top: 13px;
67 | padding-bottom: 13px;
68 | padding-left: 16px;
69 | `;
70 | const IssueCount = styled.span`
71 | font-size: 14px;
72 | :last-child {
73 | margin-left: 10px;
74 | }
75 | `;
76 |
77 | const IssueCard = styled.div`
78 | display: flex;
79 | border-bottom: 1px solid #e1e4e8;
80 | `;
81 |
82 | const IssueInfo = styled.p`
83 | font-size: 12px;
84 | color: #586069;
85 | `;
86 |
87 | const IssueDetails = styled.div`
88 | padding: 8px;
89 | `;
90 |
91 | const Icon = styled.i`
92 | color: #28a745;
93 | font-size: 20px;
94 | padding-left: 16px;
95 | padding-top: 8px;
96 | `;
97 |
98 | const NameWithOwner = styled.span`
99 | color: #586069;
100 | padding-right: 4px;
101 | font-size: 16px;
102 | `;
103 |
104 | export default Issues;
105 |
--------------------------------------------------------------------------------
/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 | import { BlockstackButton } from "react-blockstack-button";
4 | import { useBlockstack } from "react-blockstack";
5 |
6 | const LoginScreen = () => {
7 | const { signIn } = useBlockstack();
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Gitix
17 | Decentralized Home of All Git Contributions
18 |
19 | Learn more
20 |
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | const LoginContainer = styled.div`
29 | display: flex;
30 | background: #24292e;
31 | color: #fff;
32 | justify-content: center;
33 | align-items: center;
34 | height: 100vh;
35 | flex-direction: column;
36 | position: fixed;
37 | width: 100%;
38 | margin-top: -24px;
39 | `;
40 |
41 | const Title = styled.p`
42 | color: #fff;
43 | font-size: 24px;
44 | font-weight: 600;
45 | margin: 0 0 0 0;
46 | `;
47 |
48 | const Subtitle = styled.p`
49 | color: #fff;
50 | font-size: 20px;
51 | font-weight: 600;
52 | `;
53 |
54 | const LoginLink = styled.a`
55 | color: #fff;
56 | font-size: 16px;
57 | &:hover {
58 | color: #ccc;
59 | }
60 | margin-top: 20px;
61 | `;
62 | const Logo = styled.svg`
63 | fill: #fff;
64 | `;
65 |
66 | export default LoginScreen;
67 |
--------------------------------------------------------------------------------
/src/components/LoginView.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { BlockstackButton } from "react-blockstack-button";
4 | import { useBlockstack } from "react-blockstack";
5 |
6 | const LoginView = () => {
7 | const { signIn } = useBlockstack();
8 | return (
9 |
10 |
11 |
12 | );
13 | };
14 |
15 | const LoginContainer = styled.div`
16 | display: flex;
17 | justify-content: center;
18 | align-items: center;
19 | flex-direction: column;
20 | `;
21 |
22 | export default LoginView;
23 |
--------------------------------------------------------------------------------
/src/components/Nav.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback } 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 Avatar from "./Avatar";
7 | import LoginView from "./LoginView";
8 | import { useBlockstack } from "react-blockstack";
9 |
10 | const activeStyles = () => ({
11 | fontWeight: "600",
12 | color: "#fff"
13 | });
14 |
15 | const linkstyles = () => ({
16 | color: "rgba(255,255,255,0.75)",
17 | textDecoration: "none"
18 | });
19 |
20 | const ClickOutside = React.forwardRef(({ children, onClick }, ref) => {
21 | const refs = React.Children.map(children, () => React.createRef());
22 | const handleClick = useCallback(
23 | e => {
24 | console.log(refs);
25 | const isOutside = refs.every(ref => {
26 | console.log(ref);
27 | return !ref.current.contains(e.target);
28 | });
29 | if (isOutside) {
30 | onClick();
31 | }
32 | },
33 | [refs, onClick]
34 | );
35 |
36 | useEffect(() => {
37 | document.addEventListener("click", handleClick);
38 |
39 | return function() {
40 | document.removeEventListener("click", handleClick);
41 | };
42 | });
43 |
44 | return React.Children.map(children, (element, idx) =>
45 | React.cloneElement(element, { ref: refs[idx] })
46 | );
47 | });
48 |
49 | const Nav = () => {
50 | const [menuOpen, setMenuOpen] = useState(false);
51 | const { userData, signOut } = useBlockstack();
52 |
53 | const username = userData && userData.username;
54 | const address = userData && userData.identityAddress;
55 | const isSignedIn = !!signOut;
56 |
57 | const openMenu = () => {
58 | setMenuOpen(true);
59 | };
60 |
61 | const closeMenu = () => {
62 | setMenuOpen(false);
63 | };
64 |
65 | return (
66 |
67 |
68 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
86 | Pull Requests
87 |
88 |
89 |
94 | Issues
95 |
96 |
97 |
102 | + repo
103 |
104 |
105 |
106 | {isSignedIn && (
107 |
108 |
109 |
110 | {menuOpen && (
111 |
112 |
118 |
119 | )}
120 |
121 | )}
122 | {!isSignedIn && }
123 |
124 | {isSignedIn && username && (
125 |
126 | Share your public profile:{" "}
127 |
132 | https://app.gitix.org/#/u/{username}
133 |
134 |
135 | )}
136 |
137 | );
138 | };
139 |
140 | const HeaderContainer = styled.section`
141 | color: rgba(255, 255, 255, 0.75);
142 | background-color: #24292e;
143 | margin-bottom: 24px;
144 | position: relative;
145 | `;
146 |
147 | const Header = styled.div`
148 | max-width: 1012px;
149 | margin: 0 auto;
150 | padding-top: 12px;
151 | padding-bottom: 12px;
152 | align-items: center;
153 | @media (min-width: 768px) {
154 | display: flex;
155 | }
156 | `;
157 |
158 | const ShareLinkContainer = styled.div`
159 | padding: 0 20px;
160 | `;
161 |
162 | const Logo = styled.svg`
163 | fill: #fff;
164 | `;
165 |
166 | const NavContainer = styled.div`
167 | display: flex;
168 | justify-content: center;
169 | @media (min-width: 768px) {
170 | justify-content: inherit;
171 | }
172 | `;
173 |
174 | const NavItem = styled.li`
175 | padding: 0 12px;
176 | background-color: #24292e;
177 | list-style: none;
178 | font-weight: 600;
179 | font-size: 14px;
180 | text-decoration: none;
181 | &:hover {
182 | color: #fff;
183 | }
184 | `;
185 |
186 | const UserSection = styled.div`
187 | position: relative;
188 | padding: 0px 20px;
189 | `;
190 |
191 | const ShareLink = styled.a`
192 | text-decoration: none;
193 | color: #fff; #fff;
194 | `;
195 |
196 | const DropDownCaret = styled.span`
197 | display: inline-block;
198 | width: 0;
199 | height: 0;
200 | vertical-align: middle;
201 | content: "";
202 | border: 4px solid;
203 | border-right-color: transparent;
204 | border-bottom-color: transparent;
205 | border-left-color: transparent;
206 | cursor: pointer;
207 | `;
208 |
209 | export default withRouter(Nav);
210 |
--------------------------------------------------------------------------------
/src/components/Overview.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import styled from "styled-components";
3 | import LoadingIndicator from "./LoadingIndicator";
4 | import RepoCard from "./RepoCard";
5 | import {
6 | getRepositories,
7 | isUserSignedIn,
8 | getFollowing
9 | } from "../lib/blockstack";
10 |
11 | class Overview extends Component {
12 | state = { repositories: [], githubRepos: [] };
13 | componentDidMount() {
14 | if (isUserSignedIn()) {
15 | getRepositories().then(repositories => {
16 | if (repositories) {
17 | this.setState({ repositories });
18 | } else {
19 | repositories = [];
20 | }
21 | getFollowing().then(followees =>
22 | followees.map(f => {
23 | return getRepositories(f.username).then(followeeRepositories => {
24 | if (followeeRepositories) {
25 | followeeRepositories = followeeRepositories.map(r => {
26 | return { ...r, name: `${r.name} (${f.username})` };
27 | });
28 |
29 | repositories = repositories.concat(followeeRepositories);
30 | this.setState({ repositories });
31 | }
32 | return null;
33 | });
34 | })
35 | );
36 | });
37 | }
38 | }
39 |
40 | render() {
41 | const { repositories } = this.state;
42 |
43 | const repos = repositories ? (
44 | repositories.map((repo, i) => {
45 | // Only show 6 repos
46 | if (i < 6) {
47 | return ;
48 | } else {
49 | return null;
50 | }
51 | })
52 | ) : (
53 |
54 | );
55 |
56 | return (
57 |
58 |
Popular Repositories
59 | {repos.length > 0 &&
{repos}}
60 | {repos.length === 0 && (
61 |
62 | Add your first repo
63 |
64 | )}
65 |
66 |
67 |
68 |
69 | );
70 | }
71 | }
72 |
73 | const RepoContainer = styled.div`
74 | display: flex;
75 | flex-wrap: wrap;
76 | justify-content: space-between;
77 | `;
78 |
79 | const OverviewTitle = styled.p`
80 | color: #24292e;
81 | font-size: 16px;
82 | margin-bottom: 8px;
83 | `;
84 |
85 | const CalendarContainer = styled.div`
86 | position: relative;
87 | `;
88 |
89 | export default Overview;
90 |
--------------------------------------------------------------------------------
/src/components/Profile.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | const Profile = ({
5 | avatarUrl,
6 | userFullName,
7 | username,
8 | company,
9 | location,
10 | bio,
11 | organizations,
12 | contactable
13 | }) => {
14 | const organsiationList =
15 | organizations && organizations.edges && organizations.edges.length > 0
16 | ? organizations.edges.map(org => {
17 | return ;
18 | })
19 | : [];
20 |
21 | return (
22 |
23 | {avatarUrl && avatarUrl !== "" ? (
24 |
25 |
26 |
27 | ) : (
28 |
29 | )}
30 |
31 | {userFullName}
32 | {username}
33 |
34 |
35 | {bio ? bio : ""}
36 |
37 |
38 |
39 |
40 | {company && (
41 |
42 |
43 | {company}
44 |
45 | )}
46 | {location && (
47 |
48 |
49 | {location}
50 |
51 | )}
52 |
53 |
54 | {organsiationList.length > 0 && (
55 |
56 |
57 |
Organizations
58 |
59 |
60 | )}
61 |
62 | {contactable && (
63 | <>
64 |
69 | Contact
70 |
71 | >
72 | )}
73 |
74 | );
75 | };
76 |
77 | const ContactButton = styled.a`
78 | cursor: pointer;
79 | border-radius: 0.25em;
80 | color: black;
81 | background-color: #eff3f6;
82 | background-image: linear-gradient(-180deg, #fafbfc, #eff3f6 90%);
83 | font-size: 12px;
84 | line-height: 20px;
85 | padding: 3px 10px;
86 | background-position: -1px -1px;
87 | background-repeat: repeat-x;
88 | background-size: 110% 110%;
89 | border: 1px solid rgba(27, 31, 35, 0.2);
90 | display: inline-block;
91 | font-weight: 600;
92 | position: relative;
93 | vertical-align: middle;
94 | white-space: nowrap;
95 | text-decoration: none;
96 | box-sizing: border-box;
97 | margin: 8px 0px;
98 | &:hover: {
99 | text-decoration: none;
100 | }
101 | `;
102 |
103 | const ButtonIcon = styled.i``;
104 |
105 | const ProfileSection = styled.section`
106 | padding-right: 20px;
107 | padding-left: 20px;
108 | `;
109 |
110 | const NameSection = styled.div`
111 | padding: 16px 0;
112 | `;
113 |
114 | const LocationSection = styled.div`
115 | padding: 16px 0;
116 | `;
117 |
118 | const ProfileDivider = styled.div`
119 | height: 1px;
120 | margin: 8px 1px;
121 | background-color: #e1e4e8;
122 | `;
123 |
124 | const Organization = styled.p`
125 | margin: 0;
126 | font-weight: 600;
127 | font-size: 16px;
128 | `;
129 |
130 | const Avatar = styled.img`
131 | width: 35px;
132 | height: 35px;
133 | border-radius: 3px;
134 | margin-top: 2px;
135 | `;
136 |
137 | const UsersFullName = styled.p`
138 | font-weight: 600;
139 | font-size: 26px;
140 | line-height: 30px;
141 | margin: 0;
142 | `;
143 |
144 | const UsersName = styled.p`
145 | font-size: 20px;
146 | font-style: normal;
147 | font-weight: 300;
148 | line-height: 24px;
149 | color: #666;
150 | margin: 0;
151 | `;
152 |
153 | const ProfilePicContainer = styled.div`
154 | display: block;
155 | margin-left: auto;
156 | margin-right: auto;
157 | width: 50%;
158 | @media (min-width: 768px) {
159 | width: 230px;
160 | }
161 | `;
162 |
163 | const ProfilePic = styled.img`
164 | border-radius: 6px;
165 | width: 100%;
166 | max-width: 230px;
167 | `;
168 |
169 | const Placeholder = styled.div`
170 | border-radius: 6px;
171 | height: 230px;
172 | width: 230px;
173 | background: #fff;
174 | `;
175 |
176 | const Organisation = styled.p`
177 | font-weight: 600;
178 | font-size: 14px;
179 | margin: 0;
180 | `;
181 |
182 | const Location = styled.p`
183 | font-size: 14px;
184 | margin: 0;
185 | `;
186 |
187 | const Icon = styled.i`
188 | float: left;
189 | margin-right: 6px;
190 | margin-top: 3px;
191 | `;
192 |
193 | const BioContainer = styled.div`
194 | margin-bottom: 12px;
195 | max-width: 230px;
196 | font-size: 14px;
197 | color: #6a737d;
198 | `;
199 | export default Profile;
200 |
--------------------------------------------------------------------------------
/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, { useState } from "react";
2 | import styled from "styled-components";
3 | import { NavLink } from "react-router-dom";
4 |
5 | const activeStyles = () => ({
6 | fontWeight: "600",
7 | borderBottom: "2px solid #e36209",
8 | color: "#24292e"
9 | });
10 |
11 | const Linkstyles = () => ({
12 | padding: "16px 8px",
13 | marginRight: "16px",
14 | fontSize: "14px",
15 | lineHeight: "1.5",
16 | color: "#586069",
17 | textAlign: "center",
18 | textDecoration: "none"
19 | });
20 |
21 | const ProfileMenu = () => {
22 | const [viewer] = useState({
23 | repositories: null,
24 | starredRepositories: null,
25 | followers: null,
26 | following: null
27 | });
28 | return (
29 |
74 | );
75 | };
76 |
77 | const Nav = styled.nav`
78 | border-bottom: solid 1px #d1d5da;
79 | padding-bottom: 14px;
80 | `;
81 |
82 | const Counter = styled.span`
83 | padding: 2px 5px;
84 | font-size: 12px;
85 | font-weight: 600;
86 | line-height: 1;
87 | color: #586069;
88 | background-color: rgba(27, 31, 35, 0.08);
89 | border-radius: 20px;
90 | margin-left: 6px;
91 | `;
92 |
93 | export default ProfileMenu;
94 |
--------------------------------------------------------------------------------
/src/components/PullRequests.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import styled from "styled-components";
3 | import moment from "moment";
4 |
5 | const PullRequests = () => {
6 | const [pullRequests] = useState([]);
7 | const prs = pullRequests
8 | ? pullRequests.map(pr => (
9 |
10 |
11 |
12 | {pr.repository.nameWithOwner}{" "}
13 | {pr.title}
14 |
15 | opened on {`${moment(pr.publishedAt).format("ddd MMM YYYY")}`} by{" "}
16 | {`${pr.author.login}`}
17 |
18 |
19 |
20 | ))
21 | : [];
22 |
23 | const openPRs = pullRequests
24 | ? pullRequests.filter(pr => {
25 | return pr.state === "OPEN";
26 | }).length
27 | : null;
28 |
29 | const closedPRs = pullRequests
30 | ? pullRequests.filter(pr => {
31 | return pr.state === "CLOSED";
32 | }).length
33 | : null;
34 |
35 | return (
36 |
37 |
38 | Pull requests will be shown here in the future
39 | {openPRs ? `${openPRs} Open` : null}
40 | {openPRs ? `${closedPRs} Closed` : null}
41 |
42 |
{prs}
43 |
44 | );
45 | };
46 |
47 | const PRContainer = styled.section`
48 | width: 980px;
49 | margin: 0 auto;
50 | border-left: 1px solid #e1e4e8;
51 | border-right: 1px solid #e1e4e8;
52 | border-top: 1px solid #e1e4e8;
53 | `;
54 |
55 | const PRCountBG = styled.div`
56 | width: 980px;
57 | margin: 0 auto;
58 | background: #f6f8fa;
59 | border-top: 1px solid #e1e4e8;
60 | border-left: 1px solid #e1e4e8;
61 | border-right: 1px solid #e1e4e8;
62 | border-radius: 3px 3px 0 0;
63 | padding-top: 13px;
64 | padding-bottom: 13px;
65 | padding-left: 16px;
66 | `;
67 | const PRCount = styled.span`
68 | font-size: 14px;
69 | :last-child {
70 | margin-left: 10px;
71 | }
72 | `;
73 |
74 | const PRCard = styled.div`
75 | display: flex;
76 | border-bottom: 1px solid #e1e4e8;
77 | `;
78 | const PRDetailsContainer = styled.div`
79 | padding: 8px;
80 | `;
81 | const PRDetails = styled.p`
82 | font-size: 12px;
83 | color: #586069;
84 | `;
85 |
86 | const Icon = styled.i`
87 | color: #28a745;
88 | font-size: 20px;
89 | padding-left: 16px;
90 | padding-top: 8px;
91 | `;
92 |
93 | const NameWithOwner = styled.span`
94 | color: #586069;
95 | padding-right: 4px;
96 | font-size: 16px;
97 | `;
98 |
99 | export default PullRequests;
100 |
--------------------------------------------------------------------------------
/src/components/RepoCard.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import moment from "moment";
4 |
5 | const RepoCard = ({ repo, className, onDelete }) => {
6 | let gitProviderIconUrl = "/images/git.png";
7 | if (repo.url) {
8 | if (repo.url.includes("github.com")) {
9 | gitProviderIconUrl = "/images/github.png";
10 | } else if (repo.url.includes("bitbucket.org")) {
11 | gitProviderIconUrl = "/images/bitbucket.png";
12 | } else if (repo.url.includes("gitlab.com")) {
13 | gitProviderIconUrl = "/images/gitlab.png";
14 | } else if (repo.url.startsWith("https://githuman.com")) {
15 | gitProviderIconUrl = "/images/githuman.png";
16 | }
17 | }
18 | return (
19 |
20 | {" "}
21 |
22 | {repo.name}
23 |
24 | {repo.description}
25 |
26 |
27 |
28 | {repo.languages &&
29 | repo.languages[0] &&
30 | repo.languages[0].name &&
31 | repo.languages[0].name}{" "}
32 | {" "}
33 | {repo.stargazers && repo.stargazers.totalCount}{" "}
34 | {" "}
35 | {repo.forkCount}
36 |
37 | {moment(repo.updatedAt).fromNow()}
38 | {onDelete && (
39 | onDelete(repo)}>
40 | Remove
41 |
42 | )}
43 |
44 |
45 | );
46 | };
47 |
48 | const RepoCardContainer = styled.div`
49 | padding: 16px;
50 | margin-bottom: 16px;
51 |
52 | &.card {
53 | border: 1px #d1d5da solid;
54 | width: 362px;
55 | }
56 |
57 | &.list {
58 | border-bottom: 1px #d1d5da solid;
59 | }
60 | `;
61 |
62 | const RepoDescription = styled.p`
63 | font-size: 12px;
64 | color: #586069;
65 | margin: 4px 0 10px 0;
66 |
67 | &.list {
68 | font-size: 14px;
69 | }
70 | `;
71 |
72 | const RepoInfoContainer = styled.div`
73 | display: flex;
74 | `;
75 |
76 | const Circle = styled.div`
77 | visibility: collapse;
78 | height: 12px;
79 | width: 12px;
80 | border-radius: 50%;
81 | background: #f1e05a;
82 | margin-right: 5px;
83 | top: 2px;
84 | position: relative;
85 | `;
86 |
87 | const RepoLink = styled.a`
88 | font-weight: 600;
89 | font-size: 14px;
90 | color: #0366d6;
91 | cursor: pointer;
92 |
93 | &.list {
94 | font-size: 20px;
95 | }
96 | `;
97 |
98 | const RepoDetails = styled.p`
99 | visibility: collapse;
100 | color: #586069;
101 | font-size: 12px;
102 | &.card {
103 | margin: 0;
104 | }
105 | &.list {
106 | margin-bottom: 0;
107 | }
108 | `;
109 |
110 | const GitProviderIcon = styled.img`
111 | width: 16px;
112 | height: 16px;
113 | display: inline-block;
114 | vertical-align: middle;
115 | `;
116 |
117 | const Icon = styled.i`
118 | margin-left: 16px;
119 | `;
120 |
121 | const Date = styled.p`
122 | visibility: collapse;
123 | font-size: 12px;
124 | color: #586069;
125 | margin-left: 10px;
126 | margin-bottom: 0;
127 | &.card {
128 | visibility: collapse;
129 | }
130 | `;
131 |
132 | const DeleteButton = styled.a`
133 | cursor: pointer;
134 | border-radius: 0.25em;
135 | color: black;
136 | font-size: 12px;
137 | line-height: 20px;
138 | padding: 3px 10px;
139 | background-position: -1px -1px;
140 | background-repeat: repeat-x;
141 | background-size: 110% 110%;
142 | border: 1px solid rgba(27, 31, 35, 0.2);
143 | display: inline-block;
144 | font-weight: 600;
145 | position: relative;
146 | vertical-align: middle;
147 | white-space: nowrap;
148 | text-decoration: none;
149 | box-sizing: border-box;
150 | margin: 8px 0px;
151 | &:hover: {
152 | text-decoration: none;
153 | }
154 | `;
155 |
156 | const ButtonIcon = styled.i``;
157 |
158 | export default RepoCard;
159 |
--------------------------------------------------------------------------------
/src/components/Repositories.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import styled from "styled-components";
3 | import LoadingIndicator from "./LoadingIndicator";
4 | import {
5 | isUserSignedIn,
6 | getRepositories,
7 | deleteRepositories,
8 | deleteRepository
9 | } from "../lib/blockstack";
10 | import RepoCard from "./RepoCard";
11 |
12 | export const sampleRepos = [
13 | {
14 | name: "(example) react-github",
15 | owner: { username: "Example Pau1fitz (github.com)" },
16 | url: "https://github.com/Pau1fitz/react-github",
17 | description: "A Github client built with React / GraphQL",
18 | languages: [{ name: "javascript" }],
19 | stargazers: { totalCount: 43 },
20 | forkCount: 5
21 | }
22 | ];
23 | class Repo extends Component {
24 | state = {
25 | repos: [],
26 | filteredRepos: [],
27 | filtered: false,
28 | loading: true
29 | };
30 |
31 | componentDidMount() {
32 | if (isUserSignedIn()) {
33 | getRepositories().then(repos => {
34 | if (repos && repos.length > 0) {
35 | this.setState({ repos });
36 | }
37 | this.setState({ loading: false });
38 | });
39 | }
40 | }
41 |
42 | searchRepos = e => {
43 | const repos = this.state.repos.filter(repo => {
44 | if (repo.name.indexOf(e.target.value) > -1) {
45 | return repo;
46 | } else {
47 | return null;
48 | }
49 | });
50 |
51 | this.setState({
52 | filteredRepos: repos,
53 | filtered: true
54 | });
55 | };
56 |
57 | deleteRepo = repo => {
58 | deleteRepository(repo).then(() => {
59 | if (window) {
60 | window.location.href = window.location.origin;
61 | }
62 | });
63 | };
64 |
65 | render() {
66 | const { repos, filteredRepos, filtered, loading } = this.state;
67 |
68 | const visibleRepos = filtered ? filteredRepos : repos;
69 |
70 | const repositories = !loading ? (
71 | visibleRepos.map((repo, i) => {
72 | return (
73 |
79 | );
80 | })
81 | ) : (
82 |
83 | );
84 |
85 | return (
86 |
87 |
88 | Add
89 |
90 | {repos.length > 0 && (
91 |
92 |
97 |
98 | )}
99 | {repositories}
100 | {repos.length > 1 && (
101 |
{
103 | deleteRepositories().then(() => {
104 | if (window) {
105 | window.location.href = window.location.origin;
106 | }
107 | });
108 | }}
109 | >
110 | Remove all repos from gitix
111 | profile
112 |
113 | )}
114 | {repos.length === 0 && (
115 |
Add some of your git repositories here...
116 | )}
117 |
118 | );
119 | }
120 | }
121 |
122 | const AddButton = styled.a`
123 | cursor: pointer;
124 | border-radius: 0.25em;
125 | color: white;
126 | background-color: #28a745;
127 | background-image: linear-gradient(-180deg, #34d058, #28a745 90%);
128 | font-size: 12px;
129 | line-height: 20px;
130 | padding: 3px 10px;
131 | background-position: -1px -1px;
132 | background-repeat: repeat-x;
133 | background-size: 110% 110%;
134 | border: 1px solid rgba(27, 31, 35, 0.2);
135 | display: inline-block;
136 | font-weight: 600;
137 | position: relative;
138 | vertical-align: middle;
139 | white-space: nowrap;
140 | text-decoration: none;
141 | box-sizing: border-box;
142 | margin: 8px 0px;
143 | &:hover: {
144 | text-decoration: none;
145 | }
146 | `;
147 |
148 | const DeleteAllButton = styled.a`
149 | cursor: pointer;
150 | border-radius: 0.25em;
151 | color: white;
152 | background-color: #cb2431;
153 | background-image: linear-gradient(-180deg, #de4450, #cb2431 90%);
154 | font-size: 12px;
155 | line-height: 20px;
156 | padding: 3px 10px;
157 | background-position: -1px -1px;
158 | background-repeat: repeat-x;
159 | background-size: 110% 110%;
160 | border: 1px solid rgba(27, 31, 35, 0.2);
161 | display: inline-block;
162 | font-weight: 600;
163 | position: relative;
164 | vertical-align: middle;
165 | white-space: nowrap;
166 | text-decoration: none;
167 | box-sizing: border-box;
168 | margin: 8px 0px;
169 | &:hover: {
170 | text-decoration: none;
171 | }
172 | `;
173 |
174 | const SearchContainer = styled.div`
175 | border-bottom: 1px solid #d1d5da;
176 | padding-bottom: 16px;
177 | `;
178 |
179 | const SearchBox = styled.input`
180 | min-height: 34px;
181 | width: 300px;
182 | font-size: 14px;
183 | padding: 6px 8px;
184 | background-color: #fff;
185 | background-repeat: no-repeat;
186 | background-position: right 8px center;
187 | border: 1px solid #d1d5da;
188 | border-radius: 3px;
189 | outline: none;
190 | box-shadow: inset 0 1px 2px rgba(27, 31, 35, 0.075);
191 | `;
192 |
193 | const ButtonIcon = styled.i``;
194 |
195 | const PlaceHolder = styled.div`
196 | padding: 16px;
197 | margin-bottom: 16px;
198 | `;
199 |
200 | export default Repo;
201 |
--------------------------------------------------------------------------------
/src/components/Stars.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import styled from "styled-components";
3 | import moment from "moment";
4 | import LoadingIndicator from "./LoadingIndicator";
5 | import { isUserSignedIn, getFile } from "blockstack";
6 | import { sampleRepos } from "./Repositories";
7 |
8 | class Stars extends Component {
9 | state = {
10 | starredRepositories: [],
11 | filteredRepos: [],
12 | filtered: false,
13 | loading: true
14 | };
15 |
16 | componentDidMount() {
17 | if (isUserSignedIn()) {
18 | getFile("starred-repositories").then(starredRepositories => {
19 | if (starredRepositories) {
20 | this.setState({
21 | starredRepositories
22 | });
23 | } else {
24 | this.setState({
25 | starredRepositories: sampleRepos
26 | });
27 | }
28 | this.setState({ loading: false });
29 | });
30 | }
31 | }
32 |
33 | searchRepos = e => {
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 | const {
50 | starredRepositories,
51 | filteredRepos,
52 | filtered,
53 | loading
54 | } = this.state;
55 |
56 | const visibleRepos = filtered ? filteredRepos : starredRepositories;
57 |
58 | const repositories = !loading ? (
59 | visibleRepos.map((star, i) => {
60 | return (
61 |
62 |
63 | {star.owner.username} / {star.name}
64 |
65 | {star.description}
66 |
67 |
68 |
69 | {star.languages[0] &&
70 | star.languages[0] &&
71 | star.languages[0].name
72 | ? star.languages[0].name
73 | : null}
74 |
75 |
76 | {star.stargazers.totalCount.toLocaleString()}
77 |
78 | {star.forkCount.toLocaleString()}
79 | {moment(star.updatedAt).fromNow()}
80 |
81 |
82 | );
83 | })
84 | ) : (
85 |
86 | );
87 |
88 | return (
89 |
90 | {starredRepositories.length > 0 && (
91 |
92 |
97 |
98 | )}
99 | {repositories}
100 |
101 | );
102 | }
103 | }
104 |
105 | const StarCard = styled.div`
106 | border-bottom: 1px #d1d5da solid;
107 | padding: 16px;
108 | margin-bottom: 16px;
109 | `;
110 |
111 | const StarDescription = styled.p`
112 | font-size: 14px;
113 | color: #586069;
114 | margin: 4px 0 8px 0;
115 | `;
116 |
117 | const SearchContainer = styled.div`
118 | border-bottom: 1px solid #d1d5da;
119 | padding-bottom: 16px;
120 | `;
121 |
122 | const SearchBox = styled.input`
123 | min-height: 34px;
124 | width: 300px;
125 | font-size: 14px;
126 | padding: 6px 8px;
127 | background-color: #fff;
128 | background-repeat: no-repeat;
129 | background-position: right 8px center;
130 | border: 1px solid #d1d5da;
131 | border-radius: 3px;
132 | outline: none;
133 | box-shadow: inset 0 1px 2px rgba(27, 31, 35, 0.075);
134 | `;
135 |
136 | const Language = styled.span`
137 | margin-right: 10px;
138 | `;
139 |
140 | const InfoContainer = styled.div`
141 | display: flex;
142 | align-items: center;
143 | color: #586069;
144 | font-size: 12px;
145 | `;
146 |
147 | const Icon = styled.i`
148 | margin-right: 3px;
149 | color: #586069;
150 | `;
151 |
152 | const Date = styled.p`
153 | font-size: 12px;
154 | color: #586069;
155 | margin-bottom: 0;
156 | `;
157 |
158 | const Count = styled.p`
159 | font-size: 12px;
160 | color: #586069;
161 | margin-right: 12px;
162 | margin-bottom: 0;
163 | `;
164 |
165 | const Name = styled.span`
166 | font-size: 20px;
167 | `;
168 |
169 | const Owner = styled.span`
170 | font-weight: 600;
171 | font-size: 20px;
172 | `;
173 |
174 | const Circle = styled.div`
175 | height: 12px;
176 | width: 12px;
177 | border-radius: 50%;
178 | background: #f1e05a;
179 | margin-right: 5px;
180 | top: 2px;
181 | position: relative;
182 | `;
183 |
184 | const Link = styled.a`
185 | color: #0566d9;
186 | `;
187 |
188 | export default Stars;
189 |
--------------------------------------------------------------------------------
/src/components/User.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import styled from "styled-components";
3 | import LoadingIndicator from "./LoadingIndicator";
4 | import Profile from "./Profile";
5 | import RepoCard from "./RepoCard";
6 | import { Helmet } from "react-helmet";
7 | import {
8 | getGithubRepos,
9 | getRepositories,
10 | getFollowing,
11 | isUserSignedIn,
12 | lookupProfile,
13 | putFollowing,
14 | loadUserData
15 | } from "../lib/blockstack";
16 | import { getUserAppFileUrl, UserSession } from "blockstack";
17 | import { Relation } from "./models";
18 |
19 | class User extends Component {
20 | state = {
21 | repositories: [],
22 | githubRepositories: [],
23 | user: {},
24 | loading: true,
25 | loadingFollowing: true,
26 | updating: false,
27 | isFollowingUser: false,
28 | invalidUser: false,
29 | contactable: false
30 | };
31 | userSession = new UserSession();
32 |
33 | componentDidMount() {
34 | this.updateUser(this.props.match.params.user);
35 | }
36 |
37 | componentDidUpdate(prevProps) {
38 | if (this.props.match.params.user !== prevProps.match.params.user) {
39 | this.updateUser(this.props.match.params.user);
40 | }
41 | }
42 |
43 | updateUser(username) {
44 | this.setState({
45 | loadingFollowing: true,
46 | loading: true,
47 | isUserSignedIn: isUserSignedIn(),
48 | currentUser: loadUserData()
49 | });
50 | lookupProfile(username).then(
51 | user => {
52 | getUserAppFileUrl(
53 | `BlockstackUser/${username}`,
54 | username,
55 | "https://app.dmail.online"
56 | ).then(u => {
57 | this.setState({ contactable: u !== null });
58 | });
59 | this.setState({ user, invalidUser: false });
60 | if (isUserSignedIn()) {
61 | getFollowing().then(following => {
62 | const followingUserList = following.filter(
63 | u => u.username === username
64 | );
65 | this.setState({
66 | loadingFollowing: false,
67 | isFollowingUser: followingUserList.length > 0
68 | });
69 | });
70 | } else {
71 | this.setState({
72 | loadingFollowing: false,
73 | isFollowingUser: false
74 | });
75 | }
76 |
77 | getRepositories(username)
78 | .then(repositories => {
79 | if (repositories && repositories.length > 0) {
80 | this.setState({ repositories });
81 | } else {
82 | getGithubRepos(user).then(githubRepositories => {
83 | if (githubRepositories) {
84 | this.setState({ githubRepositories, loading: false });
85 | } else {
86 | this.setState({ loading: false });
87 | }
88 | });
89 | }
90 | this.setState({ loading: false });
91 | })
92 | .catch(e => {
93 | console.log(e.message);
94 | this.setState({ loading: false });
95 | });
96 | },
97 | error => {
98 | console.log({ error, invalid: "invalidUser" });
99 | this.setState({
100 | user: {
101 | name: "Invalid username"
102 | },
103 | loadingFollowing: false,
104 | loading: false,
105 | invalidUser: true
106 | });
107 | }
108 | );
109 | }
110 |
111 | followUser() {
112 | this.setState({ updating: true });
113 | const { user, currentUser } = this.state;
114 | const followPublicly = currentUser && currentUser.username;
115 | getFollowing().then(following => {
116 | const avatarUrl =
117 | (user.image && user.image.length > 0 && user.image[0].contentUrl) ||
118 | "/images/user.png";
119 | const username = this.props.match.params.user;
120 | following.push({
121 | avatarUrl,
122 | name: user.name,
123 | username,
124 | bio: user.description
125 | });
126 | if (followPublicly) {
127 | let relation = new Relation({
128 | follower: currentUser.username,
129 | followee: username
130 | });
131 | relation.save().then(saved => {
132 | console.log({ saved });
133 | });
134 | }
135 | putFollowing(following).then(
136 | this.setState({ updating: false, isFollowingUser: true })
137 | );
138 | });
139 | }
140 |
141 | unfollowUser() {
142 | this.setState({ updating: true });
143 | const { currentUser } = this.state;
144 | getFollowing().then(following => {
145 | const username = this.props.match.params.user;
146 | const newList = following.filter(f => f.username !== username);
147 | Relation.fetchList({ follower: currentUser.username, followee: username })
148 | .then(relations => {
149 | console.log({ relations });
150 | return Promise.all(relations.map(r => r.destroy()));
151 | })
152 | .catch(e => {
153 | console.log(e);
154 | })
155 | .then(() =>
156 | putFollowing(newList).then(
157 | this.setState({ updating: false, isFollowingUser: false })
158 | )
159 | );
160 | });
161 | }
162 |
163 | render() {
164 | const {
165 | repositories,
166 | githubRepositories,
167 | user,
168 | loading,
169 | updating,
170 | loadingFollowing,
171 | isFollowingUser,
172 | invalidUser,
173 | contactable,
174 | isUserSignedIn
175 | } = this.state;
176 |
177 | const overviewTitle =
178 | repositories && repositories.length > 0
179 | ? "Published Repositories"
180 | : "Github Repositories";
181 | var repos =
182 | repositories && repositories.length > 0
183 | ? repositories
184 | : githubRepositories;
185 | repos =
186 | repos && repos.length > 0
187 | ? repos.map((repo, i) => {
188 | // Only show 6 repos
189 | if (i < 6) {
190 | return ;
191 | } else {
192 | return null;
193 | }
194 | })
195 | : [];
196 |
197 | const avatarUrl =
198 | (user.image && user.image.length > 0 && user.image[0].contentUrl) ||
199 | "/images/user.png";
200 | const userFullName = user.name;
201 | const username = this.props.match.params.user;
202 | const location = null;
203 | const company = null;
204 | const bio = user.description;
205 | const organizations = [];
206 | const title = `${userFullName} | Gitix Profile`;
207 | const metaDescription = `All the git repositories publised by ${userFullName}`;
208 |
209 | return (
210 |
211 |
212 | {userFullName}
213 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
237 |
238 |
239 | {!loadingFollowing &&
240 | !isFollowingUser &&
241 | !invalidUser &&
242 | isUserSignedIn && (
243 |
this.followUser()}>
244 | Follow
245 |
246 | )}
247 | {!loadingFollowing &&
248 | isFollowingUser &&
249 | !invalidUser &&
250 | isUserSignedIn && (
251 |
this.unfollowUser()}>
252 | Unfollow
253 |
254 | )}
255 | {(loading || updating) &&
}
256 | {!loading && !invalidUser && (
257 |
258 |
259 | {repos.length > 0 && (
260 | {overviewTitle}
261 | )}
262 | {repos.length === 0 && (
263 | No repositories published yet
264 | )}
265 | {repos}
266 |
267 |
268 | )}
269 |
270 |
271 | );
272 | }
273 | }
274 |
275 | const FollowButton = styled.a`
276 | cursor: pointer;
277 | border-radius: 0.25em;
278 | color: white;
279 | background-color: #28a745;
280 | background-image: linear-gradient(-180deg, #34d058, #28a745 90%);
281 | font-size: 12px;
282 | line-height: 20px;
283 | padding: 3px 10px;
284 | background-position: -1px -1px;
285 | background-repeat: repeat-x;
286 | background-size: 110% 110%;
287 | border: 1px solid rgba(27, 31, 35, 0.2);
288 | display: inline-block;
289 | font-weight: 600;
290 | position: relative;
291 | vertical-align: middle;
292 | white-space: nowrap;
293 | text-decoration: none;
294 | box-sizing: border-box;
295 | margin: 8px 0px;
296 | &:hover: {
297 | text-decoration: none;
298 | }
299 | `;
300 |
301 | const UnfollowButton = styled.a`
302 | cursor: pointer;
303 | border-radius: 0.25em;
304 | color: black;
305 | background-color: #eff3f6;
306 | background-image: linear-gradient(-180deg, #fafbfc, #eff3f6 90%);
307 | font-size: 12px;
308 | line-height: 20px;
309 | padding: 3px 10px;
310 | background-position: -1px -1px;
311 | background-repeat: repeat-x;
312 | background-size: 110% 110%;
313 | border: 1px solid rgba(27, 31, 35, 0.2);
314 | display: inline-block;
315 | font-weight: 600;
316 | position: relative;
317 | vertical-align: middle;
318 | white-space: nowrap;
319 | text-decoration: none;
320 | box-sizing: border-box;
321 | margin: 8px 0px;
322 | &:hover: {
323 | text-decoration: none;
324 | }
325 | `;
326 |
327 | const RepoContainer = styled.div`
328 | display: flex;
329 | flex-wrap: wrap;
330 | justify-content: space-between;
331 | `;
332 |
333 | const OverviewTitle = styled.p`
334 | color: #24292e;
335 | font-size: 16px;
336 | margin-bottom: 8px;
337 | `;
338 |
339 | const ProfileContainer = styled.section`
340 | max-width: 1012px;
341 | margin: 0 auto;
342 | display: block;
343 | @media (min-width: 768px) {
344 | display: flex;
345 | }
346 | `;
347 |
348 | const InformationContainer = styled.section`
349 | margin-top: 24px;
350 | `;
351 |
352 | const ButtonIcon = styled.i``;
353 |
354 | export default User;
355 |
--------------------------------------------------------------------------------
/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 | import { signUserOut } from "blockstack";
6 |
7 | const UserMenu = React.forwardRef(
8 | ({ username, address, id, closeMenu, history }, ref) => {
9 | return (
10 |
11 | {username && (
12 |
16 | {`Signed in as ${username}`}
17 |
18 | )}
19 |
20 | {!username &&
24 | {`Signed in as ${address}`}
25 | }
26 |
27 |
28 |
29 |
30 | Your Profile
31 |
32 |
33 |
34 | Your Followers
35 |
36 |
37 |
38 | Your Stars
39 |
40 |
41 |
42 | Help
43 |
44 | {
47 | signUserOut();
48 | if (window) {
49 | window.location.href = window.location.origin;
50 | }
51 | }}
52 | >
53 | Sign Out
54 |
55 |
56 | );
57 | }
58 | );
59 |
60 | const UserMenuContainer = styled.ul`
61 | position: absolute;
62 | top: 100%;
63 | left: 0;
64 | z-index: 100;
65 | width: 160px;
66 | padding-top: 5px;
67 | padding-bottom: 5px;
68 | list-style: none;
69 | background-color: #fff;
70 | background-clip: padding-box;
71 | border: 1px solid rgba(27, 31, 35, 0.15);
72 | border-radius: 4px;
73 | box-shadow: 0 3px 12px rgba(27, 31, 35, 0.15);
74 | width: 180px;
75 | margin-top: 8px;
76 | `;
77 |
78 | const DropDownItem = styled.li`
79 | cursor: pointer;
80 | display: block;
81 | padding: 4px 10px 4px 15px;
82 | overflow: hidden;
83 | color: #24292e;
84 | text-overflow: ellipsis;
85 | white-space: nowrap;
86 | &:hover {
87 | color: #fff;
88 | background-color: #0366d6;
89 | }
90 | `;
91 |
92 | const DropDownDivider = styled.li`
93 | height: 1px;
94 | margin: 8px 1px;
95 | background-color: #e1e4e8;
96 | `;
97 |
98 | const Link = styled.a`
99 | text-decoration: none;
100 | `;
101 |
102 | const withRouterAndRef = Wrapped => {
103 | const WithRouter = withRouter(({ forwardRef, ...otherProps }) => (
104 |
105 | ));
106 | const WithRouterAndRef = React.forwardRef((props, ref) => (
107 |
108 | ));
109 | const name = Wrapped.displayName || Wrapped.name;
110 | WithRouterAndRef.displayName = `withRouterAndRef(${name})`;
111 | return WithRouterAndRef;
112 | };
113 |
114 | export default withRouterAndRef(UserMenu);
115 |
--------------------------------------------------------------------------------
/src/components/constants.js:
--------------------------------------------------------------------------------
1 |
2 | export const RADIKS_SERVER_URL = process.env.GITIX_RADIKS_SERVER
3 | ? process.env.GITIX_RADIKS_SERVER
4 | : 'https://gitix.herokuapp.com'
5 |
--------------------------------------------------------------------------------
/src/components/models.js:
--------------------------------------------------------------------------------
1 | import { Model } from "radiks";
2 |
3 | export class Relation extends Model {
4 | static className = "Relation";
5 | static validateUsername = true;
6 | static schema = {
7 | follower: { type: String, decrypted: true },
8 | followee: { type: String, decrypted: true }
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/src/images/octocat.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/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 { HashRouter } from "react-router-dom";
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById("root")
12 | );
13 |
--------------------------------------------------------------------------------
/src/lib/blockstack.js:
--------------------------------------------------------------------------------
1 | import { AppConfig, lookupProfile as bsLookupProfile } from "blockstack";
2 | import { configure, getConfig } from "radiks";
3 | import { initBlockstack } from "react-blockstack";
4 | import { RADIKS_SERVER_URL } from "../components/constants";
5 | import { sampleRepos } from "../components/Repositories";
6 |
7 | const appConfig = new AppConfig(
8 | ["store_write", "publish_data"],
9 | typeof window !== "undefined"
10 | ? window.location.origin
11 | : "http://localhost:8000",
12 | typeof window !== "undefined"
13 | ? "/"
14 | : window.location.pathname + window.location.search + window.location.hash,
15 | "/manifest.json"
16 | );
17 |
18 | const { userSession } = initBlockstack({ appConfig });
19 |
20 | configure({
21 | apiServer: RADIKS_SERVER_URL,
22 | userSession
23 | });
24 |
25 | export const isBrowser = () => typeof window !== "undefined";
26 |
27 | export const loadUserData = () => {
28 | const { userSession } = getConfig();
29 | return isBrowser() && userSession.isUserSignedIn()
30 | ? userSession.loadUserData()
31 | : {};
32 | };
33 |
34 | export const isUserSignedIn = () => {
35 | const { userSession } = getConfig();
36 | return isBrowser() && userSession.isUserSignedIn();
37 | };
38 |
39 | export const lookupProfile = username => bsLookupProfile(username);
40 |
41 | export const getRepositories = username =>
42 | userSession
43 | .getFile("repositories", { decrypt: false, username })
44 | .then(repositories => {
45 | if (repositories) {
46 | const repoArray = JSON.parse(repositories);
47 | if (Array.isArray(repoArray)) {
48 | return repoArray;
49 | } else {
50 | return [
51 | {
52 | name: "[outdated-repository-list-schema]",
53 | owner: { username: "gitix admin" },
54 | url: "https://github.com/friedger/gitix",
55 | description:
56 | "Please remove all repos from your gitix profile and start again. Sorry!",
57 | languages: [{ name: "(error)" }],
58 | stargazers: { totalCount: 0 },
59 | forkCount: 0
60 | }
61 | ];
62 | }
63 | } else {
64 | return null;
65 | }
66 | })
67 | .catch(e => {
68 | if (username && e.message === "Missing readURL") {
69 | return null;
70 | }
71 | return sampleRepos;
72 | });
73 |
74 | export const putRepositories = repositories => {
75 | userSession.putFile("repositories", JSON.stringify(repositories), {
76 | encrypt: false
77 | });
78 | };
79 |
80 | export const deleteRepositories = () => {
81 | return userSession.deleteFile("repositories");
82 | };
83 |
84 | export const putNewRepository = repo => {
85 | return getRepositories().then(repoList => {
86 | if (!repoList) {
87 | repoList = [];
88 | }
89 | repoList.push(repo);
90 | return putRepositories(repoList);
91 | });
92 | };
93 |
94 | export const deleteRepository = repo => {
95 | return getRepositories().then(repoList => {
96 | if (!repoList) {
97 | repoList = [];
98 | }
99 | repoList = repoList.filter(r => repo.url !== r.url);
100 | return putRepositories(repoList);
101 | });
102 | };
103 |
104 | export const getFollowing = () =>
105 | userSession.getFile("following").then(f => {
106 | let following;
107 | if (!f) {
108 | following = [];
109 | } else {
110 | following = JSON.parse(f);
111 | }
112 | return following;
113 | });
114 |
115 | export const putFollowing = following =>
116 | userSession.putFile("following", JSON.stringify(following));
117 |
118 | export const getGithubRepos = profile => {
119 | if (profile && profile.account) {
120 | const githubAccounts = profile.account.filter(a => a.service === "github");
121 |
122 | if (githubAccounts.length > 0) {
123 | return fetch(
124 | `https://api.github.com/users/${githubAccounts[0].identifier}/repos?sort=pushed`
125 | )
126 | .then(response => response.json())
127 | .then(githubRepos => {
128 | return githubRepos.map(ghRepo => {
129 | return {
130 | name: ghRepo.name,
131 | owner: { username: ghRepo.owner.login },
132 | url: ghRepo.html_url,
133 | description: ghRepo.description,
134 | languages: [{ name: ghRepo.language }],
135 | stargazers: { totalCount: ghRepo.stargazers_count },
136 | forkCount: ghRepo.forks_count
137 | };
138 | });
139 | });
140 | }
141 | }
142 | console.log("no github account");
143 | return Promise.resolve();
144 | };
145 |
--------------------------------------------------------------------------------