├── src ├── shared │ ├── utils │ │ ├── index.js │ │ └── makeId.js │ └── components │ │ ├── Highlight │ │ └── index.js │ │ ├── LoadingOverlay │ │ └── index.jsx │ │ └── ScrollBar │ │ └── index.jsx ├── assets │ ├── favicon.png │ └── favicon.xcf ├── themes │ ├── light.js │ ├── dark.js │ └── index.js ├── redux │ ├── api │ │ ├── github │ │ │ ├── index.js │ │ │ ├── getClient.js │ │ │ ├── utils.js │ │ │ ├── searchAccount.js │ │ │ ├── getProfile.js │ │ │ ├── getRepositories.js │ │ │ ├── getCommits.js │ │ │ ├── getBranches.js │ │ │ └── api.github.test.js │ │ └── githubGQL │ │ │ ├── index.js │ │ │ ├── .graphqlconfig.template │ │ │ ├── getClient.js │ │ │ ├── utils.js │ │ │ ├── getCommits │ │ │ ├── query.graphql │ │ │ └── index.js │ │ │ ├── getBranches │ │ │ ├── query.graphql │ │ │ └── index.js │ │ │ ├── getProfile │ │ │ ├── query.graphql │ │ │ └── index.js │ │ │ ├── searchAccount │ │ │ ├── index.js │ │ │ └── query.graphql │ │ │ ├── getRepositories │ │ │ ├── index.js │ │ │ └── query.graphql │ │ │ └── api.githubGQL.test.js │ ├── reducers.js │ ├── utils │ │ ├── withCancellation.js │ │ ├── index.js │ │ ├── redux.utils.test.js │ │ └── createSlice.js │ ├── modules │ │ ├── index.js │ │ └── profiles.js │ └── index.js ├── components │ ├── Layout.jsx │ ├── Header │ │ ├── FetchTopUser.js │ │ ├── StepsBar │ │ │ ├── Panel.jsx │ │ │ ├── index.jsx │ │ │ ├── StepUser.jsx │ │ │ ├── StepRepo.jsx │ │ │ ├── Step.jsx │ │ │ └── StepShow.jsx │ │ ├── index.jsx │ │ ├── UserBar.jsx │ │ ├── User.jsx │ │ └── UserSearch.jsx │ ├── Main.jsx │ └── App │ │ └── index.jsx ├── routes │ ├── index.jsx │ └── PrivateRoute.jsx ├── index.jsx ├── setupTests.js └── serviceWorker.js ├── .env ├── old ├── article │ ├── 4.gif │ ├── ml.png │ ├── p2s.png │ ├── ss_d3.png │ ├── ss_mc.png │ ├── vis_repo.png │ ├── ss_jquery.png │ ├── vis_history.png │ ├── vis_repo_pre.png │ ├── vis_history_pre.png │ └── vis_history_person.png ├── favicon.ico ├── favicon.png ├── resource │ ├── ss_d3.gif │ ├── ss_d3.png │ ├── ss_mc.png │ ├── default.png │ ├── forkme.png │ ├── particle.png │ ├── ss_jquery.png │ ├── ss_rails.png │ ├── thumbnail.png │ ├── preloader24.gif │ ├── ss_bootstrap.png │ ├── github_webfont.eot │ ├── github_webfont.ttf │ ├── github_webfont.woff │ ├── github_webfont_ie.eot │ ├── ss_song-of-github.png │ └── octocat-spinner-128.gif ├── js │ ├── cookies.js │ ├── jsonp.js │ ├── vis.js │ ├── ghcs.js │ ├── langhg.js │ ├── md5.js │ ├── repo.js │ └── usercommit.js └── tree.js ├── public ├── robots.txt └── index.html ├── .eslintignore ├── jsconfig.json ├── config ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── pnpTs.js ├── getHttpsConfig.js ├── paths.js ├── env.js ├── modules.js └── webpackDevServer.config.js ├── .gitignore ├── jest.config.json ├── scripts ├── test.js ├── start.js └── build.js ├── README.md ├── .eslintrc ├── package.json └── LICENSE.txt /src/shared/utils/index.js: -------------------------------------------------------------------------------- 1 | export * from './makeId'; 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_CLIENT_ID= 2 | REACT_APP_CLIENT_SECRET= 3 | REACT_APP_PERSONAL_TOKEN= 4 | -------------------------------------------------------------------------------- /old/article/4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/article/4.gif -------------------------------------------------------------------------------- /old/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/favicon.ico -------------------------------------------------------------------------------- /old/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/favicon.png -------------------------------------------------------------------------------- /old/article/ml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/article/ml.png -------------------------------------------------------------------------------- /old/article/p2s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/article/p2s.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /old/article/ss_d3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/article/ss_d3.png -------------------------------------------------------------------------------- /old/article/ss_mc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/article/ss_mc.png -------------------------------------------------------------------------------- /old/resource/ss_d3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/resource/ss_d3.gif -------------------------------------------------------------------------------- /old/resource/ss_d3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/resource/ss_d3.png -------------------------------------------------------------------------------- /old/resource/ss_mc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/resource/ss_mc.png -------------------------------------------------------------------------------- /src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/src/assets/favicon.png -------------------------------------------------------------------------------- /src/assets/favicon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/src/assets/favicon.xcf -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | old/ 2 | config/ 3 | scripts/ 4 | build/ 5 | src/serviceWorker.js 6 | src/setupTests.js 7 | -------------------------------------------------------------------------------- /old/article/vis_repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/article/vis_repo.png -------------------------------------------------------------------------------- /old/resource/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/resource/default.png -------------------------------------------------------------------------------- /old/resource/forkme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/resource/forkme.png -------------------------------------------------------------------------------- /old/article/ss_jquery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/article/ss_jquery.png -------------------------------------------------------------------------------- /old/article/vis_history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/article/vis_history.png -------------------------------------------------------------------------------- /old/resource/particle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/resource/particle.png -------------------------------------------------------------------------------- /old/resource/ss_jquery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/resource/ss_jquery.png -------------------------------------------------------------------------------- /old/resource/ss_rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/resource/ss_rails.png -------------------------------------------------------------------------------- /old/resource/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/resource/thumbnail.png -------------------------------------------------------------------------------- /old/article/vis_repo_pre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/article/vis_repo_pre.png -------------------------------------------------------------------------------- /old/resource/preloader24.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/resource/preloader24.gif -------------------------------------------------------------------------------- /old/resource/ss_bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/resource/ss_bootstrap.png -------------------------------------------------------------------------------- /old/article/vis_history_pre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/article/vis_history_pre.png -------------------------------------------------------------------------------- /old/resource/github_webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/resource/github_webfont.eot -------------------------------------------------------------------------------- /old/resource/github_webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/resource/github_webfont.ttf -------------------------------------------------------------------------------- /old/resource/github_webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/resource/github_webfont.woff -------------------------------------------------------------------------------- /old/article/vis_history_person.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/article/vis_history_person.png -------------------------------------------------------------------------------- /old/resource/github_webfont_ie.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/resource/github_webfont_ie.eot -------------------------------------------------------------------------------- /old/resource/ss_song-of-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/resource/ss_song-of-github.png -------------------------------------------------------------------------------- /old/resource/octocat-spinner-128.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artzub/GitHubVisualizer/HEAD/old/resource/octocat-spinner-128.gif -------------------------------------------------------------------------------- /src/themes/light.js: -------------------------------------------------------------------------------- 1 | import dark from '@/themes/dark'; 2 | 3 | export default { 4 | ...dark, 5 | 6 | name: 'light', 7 | palette: { 8 | type: 'light', 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/redux/api/github/index.js: -------------------------------------------------------------------------------- 1 | export * from './searchAccount'; 2 | export * from './getProfile'; 3 | export * from './getRepositories'; 4 | export * from './getBranches'; 5 | export * from './getCommits'; 6 | -------------------------------------------------------------------------------- /src/redux/api/githubGQL/index.js: -------------------------------------------------------------------------------- 1 | export * from './searchAccount'; 2 | export * from './getProfile'; 3 | export * from './getRepositories'; 4 | export * from './getBranches'; 5 | export * from './getCommits'; 6 | -------------------------------------------------------------------------------- /src/themes/dark.js: -------------------------------------------------------------------------------- 1 | import blue from '@material-ui/core/colors/blue'; 2 | 3 | export default { 4 | name: 'dark', 5 | palette: { 6 | type: 'dark', 7 | primary: { 8 | main: blue[200], 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "allowSyntheticDefaultImports": false, 5 | "baseUrl": "src", 6 | "paths": { 7 | "@/*": ["*"] 8 | } 9 | }, 10 | "exclude": ["node_modules", "build"] 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '@/components/Header'; 3 | import Main from '@/components/Main'; 4 | 5 | const Layout = () => ( 6 | 7 |
8 |
9 | 10 | ); 11 | 12 | export default Layout; 13 | -------------------------------------------------------------------------------- /src/redux/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { reducers as sliceReducers } from './modules'; 3 | 4 | // WARNING: Put reducers for modules as modules in the index.js file. 5 | const rootReducer = combineReducers({ 6 | ...sliceReducers, 7 | }); 8 | 9 | export { 10 | rootReducer, 11 | }; 12 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/routes/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from '@/components/Layout'; 3 | import { Switch, Route, BrowserRouter } from 'react-router-dom'; 4 | 5 | const AppRouter = () => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default AppRouter; 16 | -------------------------------------------------------------------------------- /src/components/Header/FetchTopUser.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import slice from '@/redux/modules/profiles'; 3 | import { useDispatch } from "react-redux"; 4 | 5 | const FetchTopUser = () => { 6 | const dispatch = useDispatch(); 7 | 8 | useEffect( 9 | () => { 10 | dispatch(slice.actions.fetchTop()); 11 | }, 12 | [dispatch], 13 | ); 14 | 15 | return null; 16 | }; 17 | 18 | export default FetchTopUser; 19 | -------------------------------------------------------------------------------- /src/components/Main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { default as ContainerOriginal } from '@material-ui/core/Container'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | 5 | const Container = withStyles({ 6 | root: { 7 | paddingTop: '64px', 8 | }, 9 | })(ContainerOriginal); 10 | 11 | const Main = () => ( 12 | 13 | Sometext 14 | 15 | ); 16 | 17 | export default Main; 18 | -------------------------------------------------------------------------------- /src/redux/api/githubGQL/.graphqlconfig.template: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Untitled GraphQL Schema", 3 | "schemaPath": "schema.graphql", 4 | "extensions": { 5 | "endpoints": { 6 | "Github GraphGL": { 7 | "url": "https://api.github.com/graphql", 8 | "headers": { 9 | "user-agent": "JS GraphQL", 10 | "authorization": "token PERSONAL_TOKEN" 11 | }, 12 | "introspect": false 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/redux/utils/withCancellation.js: -------------------------------------------------------------------------------- 1 | import { CANCEL } from 'redux-saga'; 2 | 3 | /** 4 | * @param {function(signal: AbortSignal): Promise<*>} method 5 | * @return {Promise<*>} 6 | */ 7 | export const withCancellation = (method) => { 8 | const abortController = new AbortController(); 9 | 10 | const result = method(abortController.signal); 11 | 12 | if (result) { 13 | result[CANCEL] = () => abortController.abort(); 14 | } 15 | return result; 16 | }; 17 | -------------------------------------------------------------------------------- /src/redux/api/githubGQL/getClient.js: -------------------------------------------------------------------------------- 1 | import { Octokit } from '@octokit/rest'; 2 | 3 | let instance; 4 | let lastToken; 5 | 6 | export default () => { 7 | const token = localStorage.getItem('user_token'); 8 | 9 | if (!instance || lastToken !== token) { 10 | lastToken = token || process.env.REACT_APP_PERSONAL_TOKEN; 11 | instance = new Octokit({ 12 | userAgent: 'visgit/v1.0.0', 13 | auth: lastToken, 14 | }); 15 | } 16 | 17 | return instance; 18 | }; 19 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from '@/components/App'; 3 | import ReactDOM from 'react-dom'; 4 | import * as serviceWorker from './serviceWorker'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | 8 | // If you want your app to work offline and load faster, you can change 9 | // unregister() to register() below. Note this comes with some pitfalls. 10 | // Learn more about service workers: https://bit.ly/CRA-PWA 11 | serviceWorker.unregister(); 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | *.iws 4 | test/ 5 | *.secret 6 | 7 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 8 | 9 | # dependencies 10 | /node_modules 11 | /.pnp 12 | .pnp.js 13 | 14 | # testing 15 | /coverage 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | .env.local 23 | .env.development.local 24 | .env.test.local 25 | .env.production.local 26 | 27 | *.local 28 | .graphqlconfig 29 | 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | -------------------------------------------------------------------------------- /src/redux/api/githubGQL/utils.js: -------------------------------------------------------------------------------- 1 | export const addCursorAfter = (text, lastCursor) => { 2 | // eslint-disable-next-line no-new-func 3 | const parser = new Function(`return \`${text}\`;`); 4 | return parser.call({ 5 | cursorArgument: lastCursor ? '$page: String!' : '', 6 | after: lastCursor ? 'after: $page' : '', 7 | }); 8 | }; 9 | 10 | export const parseRateLimit = ({ cost, remaining, resetAt } = {}) => ({ 11 | limit: 5000, 12 | remaining, 13 | cost, 14 | resetAt: resetAt && +(new Date(resetAt)), 15 | }); 16 | -------------------------------------------------------------------------------- /src/shared/utils/makeId.js: -------------------------------------------------------------------------------- 1 | const defaultAlphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; 2 | 3 | /** 4 | * Make a random ID, similar to a UUID, except with variable length and less complex 5 | * @param length length of id 6 | * @param alphabet string of characters to choose from 7 | */ 8 | export const makeId = (length = 10, alphabet = defaultAlphabet) => 9 | new Array(length) 10 | .fill('') 11 | .map(() => alphabet[ Math.floor(Math.random() * alphabet.length) ]) 12 | .join(''); 13 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | 7 | // Do this as the first thing so that any code reading it knows the right env. 8 | process.env.BABEL_ENV = 'test'; 9 | process.env.NODE_ENV = 'test'; 10 | process.env.PUBLIC_URL = ''; 11 | 12 | // Ensure environment variables are read. 13 | require('../config/env'); 14 | -------------------------------------------------------------------------------- /src/redux/api/github/getClient.js: -------------------------------------------------------------------------------- 1 | import { createOAuthAppAuth } from "@octokit/auth-oauth-app"; 2 | import { Octokit } from '@octokit/rest'; 3 | 4 | let instance; 5 | 6 | export default () => { 7 | if (instance) { 8 | return instance; 9 | } 10 | 11 | instance = new Octokit({ 12 | userAgent: 'visgit/v1.0.0', 13 | authStrategy: createOAuthAppAuth, 14 | auth: { 15 | type: 'oauth-app', 16 | clientId: process.env.REACT_APP_CLIENT_ID, 17 | clientSecret: process.env.REACT_APP_CLIENT_SECRET, 18 | }, 19 | }); 20 | 21 | return instance; 22 | }; 23 | -------------------------------------------------------------------------------- /src/redux/api/github/utils.js: -------------------------------------------------------------------------------- 1 | export const parseRateLimit = (headers = {}) => ({ 2 | limit: +headers['x-ratelimit-limit'], 3 | remaining: +headers['x-ratelimit-remaining'], 4 | resetAt: +headers['x-ratelimit-reset'] * 1000, 5 | }); 6 | 7 | const reg = /page=(\d+)>; rel="next"/; 8 | export const parsePageInfo = ({ link = '' } = {}) => { 9 | const hasNextPage = Boolean(link && link.includes('rel="next"')); 10 | let nextPage; 11 | 12 | if (hasNextPage) { 13 | nextPage = +link.match(reg)[1]; 14 | } 15 | 16 | return { 17 | nextPage, 18 | hasNextPage, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/redux/api/githubGQL/getCommits/query.graphql: -------------------------------------------------------------------------------- 1 | query($owner:String!, $repo:String!, $branch: String!, $perPage: Int!, ${this.cursorArgument}) { 2 | rateLimit{ 3 | cost 4 | remaining 5 | resetAt 6 | } 7 | repository(owner: $owner, name: $repo) { 8 | ref(qualifiedName: $branch) { 9 | target { 10 | ... on Commit { 11 | history(first: $perPage, ${this.after}) { 12 | nodes { 13 | oid 14 | } 15 | pageInfo { 16 | nextPage: endCursor, 17 | hasNextPage 18 | } 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/redux/api/githubGQL/getBranches/query.graphql: -------------------------------------------------------------------------------- 1 | query($owner: String!, $repo: String!, $perPage: Int!, ${this.cursorArgument}) { 2 | rateLimit{ 3 | cost 4 | remaining 5 | resetAt 6 | } 7 | repository(owner: $owner, name: $repo) { 8 | refs(refPrefix: "refs/heads/", first: $perPage, ${this.after}) { 9 | totalCount 10 | nodes { 11 | name 12 | target { 13 | ...on Commit { 14 | history { 15 | totalCount 16 | } 17 | } 18 | } 19 | } 20 | pageInfo { 21 | nextPage: endCursor, 22 | hasNextPage 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/redux/api/githubGQL/getProfile/query.graphql: -------------------------------------------------------------------------------- 1 | query($login: String!, $isOrganization: Boolean!) { 2 | rateLimit{ 3 | cost 4 | remaining 5 | resetAt 6 | } 7 | organization(login: $login) @include(if: $isOrganization) { 8 | ...repositoriesData 9 | ...profileData 10 | }, 11 | user(login: $login) @skip(if: $isOrganization) { 12 | ...repositoriesData 13 | ...profileData 14 | }, 15 | } 16 | 17 | fragment profileData on ProfileOwner { 18 | id 19 | name 20 | login 21 | location 22 | websiteUrl 23 | } 24 | 25 | fragment repositoriesData on RepositoryOwner { 26 | avatarUrl(size: 128) 27 | url 28 | repositories { 29 | totalCount 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/redux/api/github/searchAccount.js: -------------------------------------------------------------------------------- 1 | import { withCancellation } from "@/redux/utils"; 2 | 3 | import getClient from './getClient'; 4 | import { parseRateLimit } from "./utils"; 5 | 6 | /** 7 | * Searches accounts by text 8 | * @param {String} search 9 | * @return {Promise<{rateLimit: *, data: Array}>} 10 | */ 11 | export const searchAccount = (search) => withCancellation(async (signal) => { 12 | const client = getClient(); 13 | 14 | const data = await client.search.users({ 15 | q: search, 16 | per_page: 10, 17 | request: { 18 | signal, 19 | }, 20 | }); 21 | 22 | return { 23 | data: data?.data?.items || [], 24 | rateLimit: parseRateLimit(data?.headers), 25 | }; 26 | }); 27 | -------------------------------------------------------------------------------- /src/redux/utils/index.js: -------------------------------------------------------------------------------- 1 | export { default as createSlice } from './createSlice'; 2 | export { createSelector } from '@reduxjs/toolkit'; 3 | export * from './withCancellation'; 4 | 5 | /** 6 | * incs counter of request and set isFetching to true 7 | * @param state 8 | */ 9 | export const startFetching = (state) => { 10 | state.isFetching = true; 11 | state._requests = (state._requests ?? 0) + 1; 12 | state.error = ''; 13 | }; 14 | 15 | /** 16 | * decs counter of request and set isFetching to false if counter less than 1. 17 | * @param state 18 | */ 19 | export const stopFetching = (state) => { 20 | state._requests = Math.max(0, (state._requests ?? 1) - 1); 21 | state.isFetching = !!state._requests; 22 | }; 23 | -------------------------------------------------------------------------------- /src/redux/api/github/getProfile.js: -------------------------------------------------------------------------------- 1 | import { withCancellation } from "@/redux/utils"; 2 | import getClient from './getClient'; 3 | import { parseRateLimit } from "./utils"; 4 | 5 | /** 6 | * Gets profile by owner's login 7 | * @param {String} login - login of a user 8 | * @return {Promise<{rateLimit: *, data: object}>} 9 | */ 10 | export const getProfile = (login) => 11 | withCancellation(async (signal) => { 12 | const client = getClient(); 13 | 14 | const data = await client.users.getByUsername({ 15 | username: login, 16 | request: { 17 | signal, 18 | }, 19 | }); 20 | 21 | return { 22 | data: data?.data, 23 | rateLimit: parseRateLimit(data?.headers), 24 | }; 25 | }); 26 | -------------------------------------------------------------------------------- /src/redux/api/githubGQL/searchAccount/index.js: -------------------------------------------------------------------------------- 1 | import { parseRateLimit } from "@/redux/api/githubGQL/utils"; 2 | import { withCancellation } from "@/redux/utils"; 3 | import getClient from '../getClient'; 4 | import query from './query.graphql'; 5 | 6 | /** 7 | * Searches accounts by text 8 | * @param {String} search 9 | * @return {Promise<{rateLimit: *, data: Array}>} 10 | */ 11 | export const searchAccount = (search) => withCancellation(async (signal) => { 12 | const client = getClient(); 13 | 14 | const data = await client.graphql(query, { 15 | search, 16 | request: { 17 | signal, 18 | }, 19 | }); 20 | 21 | return { 22 | data: data?.search?.nodes || [], 23 | rateLimit: parseRateLimit(data?.rateLimit), 24 | }; 25 | }); 26 | -------------------------------------------------------------------------------- /config/pnpTs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { resolveModuleName } = require('ts-pnp'); 4 | 5 | exports.resolveModuleName = ( 6 | typescript, 7 | moduleName, 8 | containingFile, 9 | compilerOptions, 10 | resolutionHost 11 | ) => { 12 | return resolveModuleName( 13 | moduleName, 14 | containingFile, 15 | compilerOptions, 16 | resolutionHost, 17 | typescript.resolveModuleName 18 | ); 19 | }; 20 | 21 | exports.resolveTypeReferenceDirective = ( 22 | typescript, 23 | moduleName, 24 | containingFile, 25 | compilerOptions, 26 | resolutionHost 27 | ) => { 28 | return resolveModuleName( 29 | moduleName, 30 | containingFile, 31 | compilerOptions, 32 | resolutionHost, 33 | typescript.resolveTypeReferenceDirective 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/redux/api/githubGQL/searchAccount/query.graphql: -------------------------------------------------------------------------------- 1 | query($search: String!) { 2 | rateLimit{ 3 | cost 4 | remaining 5 | resetAt 6 | } 7 | search(first: 10, query: $search, type: USER) { 8 | nodes { 9 | type: __typename, 10 | ... on User { 11 | ...profileData 12 | ...repositoriesData 13 | }, 14 | ... on Organization { 15 | ...profileData 16 | ...repositoriesData 17 | } 18 | } 19 | }, 20 | } 21 | 22 | fragment profileData on ProfileOwner { 23 | id 24 | name 25 | login 26 | } 27 | 28 | fragment repositoriesData on RepositoryOwner { 29 | avatarUrl(size: 128) 30 | all: repositories { 31 | totalCount 32 | }, 33 | private: repositories(privacy: PRIVATE) { 34 | totalCount 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/shared/components/Highlight/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeId } from "@/shared/utils/makeId"; 3 | import styled from "styled-components"; 4 | 5 | const Highlighted = styled.span` 6 | color: ${({ theme }) => theme.palette.primary.light} 7 | `; 8 | 9 | const Highlight = ({ search, text }) => { 10 | const searchLowerCase = `${search}`.toLowerCase(); 11 | // eslint-disable-next-line security/detect-non-literal-regexp 12 | const reg = new RegExp(`(${escape(searchLowerCase)})`, 'ugi'); 13 | const textParts = `${text}`.split(reg); 14 | return textParts.map((textPart) => textPart.toLowerCase() === searchLowerCase ? ( 15 | 16 | {textPart} 17 | 18 | ) : textPart); 19 | }; 20 | 21 | export default Highlight; 22 | -------------------------------------------------------------------------------- /src/redux/api/githubGQL/getProfile/index.js: -------------------------------------------------------------------------------- 1 | import { withCancellation } from "@/redux/utils"; 2 | import getClient from '../getClient'; 3 | import query from './query.graphql'; 4 | 5 | /** 6 | * Gets profile by owner's login 7 | * @param {String} login - login of a user 8 | * @param {Boolean} [isOrganization] - if true then receiving organization 9 | * @return {Promise<{rateLimit: *, data: object}>} 10 | */ 11 | export const getProfile = (login, isOrganization = false) => 12 | withCancellation(async (singal) => { 13 | const client = getClient(); 14 | 15 | const data = await client.graphql(query, { 16 | login, 17 | isOrganization, 18 | }); 19 | 20 | return { 21 | data: data?.organization || data?.user, 22 | rateLimit: data?.rateLimit, 23 | }; 24 | }); 25 | -------------------------------------------------------------------------------- /src/redux/modules/index.js: -------------------------------------------------------------------------------- 1 | import { all } from 'redux-saga/effects'; 2 | import profiles from "./profiles"; 3 | 4 | // Put modules that have their reducers nested in other (root) reducers here 5 | const nestedSlices = []; 6 | 7 | // Put modules whose reducers you want in the root tree in this array. 8 | const rootSlices = [ 9 | profiles, 10 | ]; 11 | 12 | const sagas = [...rootSlices, ...nestedSlices] 13 | .map((slice) => slice.sagas) 14 | .reduce((acc, sagas) => [...acc, ...sagas]); 15 | 16 | export function* rootSaga() { 17 | yield all(sagas.map((saga) => saga())); 18 | } 19 | 20 | function getReducers() { 21 | const reducerObj = {}; 22 | rootSlices.forEach((slice) => { 23 | reducerObj[slice.name] = slice.reducer; 24 | }); 25 | return reducerObj; 26 | } 27 | 28 | export const reducers = getReducers(); 29 | -------------------------------------------------------------------------------- /src/routes/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Route } from 'react-router-dom'; 4 | 5 | const PrivateRoute = (props) => { 6 | const { render, component: Component, ...rest } = props; 7 | 8 | const routeRender = useCallback( 9 | (routeProps) => Component ? : render(routeProps), 10 | [Component, render], 11 | ); 12 | 13 | return ( 14 | 18 | ); 19 | }; 20 | 21 | PrivateRoute.propTypes = { 22 | component: PropTypes.oneOfType([ 23 | PropTypes.string, 24 | PropTypes.func, 25 | PropTypes.object, 26 | ]), 27 | render: PropTypes.func, 28 | }; 29 | 30 | PrivateRoute.defaultProps = { 31 | component: null, 32 | render: () => {}, 33 | }; 34 | 35 | export default PrivateRoute; 36 | -------------------------------------------------------------------------------- /src/redux/api/github/getRepositories.js: -------------------------------------------------------------------------------- 1 | import { parseRateLimit, parsePageInfo } from "@/redux/api/github/utils"; 2 | import { withCancellation } from "@/redux/utils"; 3 | import getClient from './getClient'; 4 | 5 | /** 6 | * Gets repositories of an owner 7 | * @param {String} owner - login of a user of an organization 8 | * @param {Number} [perPage] - page size, default 10, (max is 100) 9 | * @param {Number} [page] - index of page 10 | * @return {Promise<{rateLimit: *, data: Array, pageInfo: *}>} 11 | */ 12 | export const getRepositories = ({ owner, perPage = 10, page }) => 13 | withCancellation(async (signal) => { 14 | const client = getClient(); 15 | 16 | const data = await client.repos.listForUser({ 17 | username: owner, 18 | per_page: perPage, 19 | page, 20 | request: { 21 | signal, 22 | }, 23 | }); 24 | 25 | return { 26 | data: data?.data || [], 27 | pageInfo: parsePageInfo(data?.headers), 28 | rateLimit: parseRateLimit(data?.headers), 29 | }; 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/Header/StepsBar/Panel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles } from '@material-ui/core/styles'; 3 | import Alert from '@material-ui/lab/Alert'; 4 | import PropTypes from 'prop-types'; 5 | 6 | const AlertStyled = withStyles(() => ({ 7 | root: { 8 | marginBottom: '8px', 9 | }, 10 | }))(Alert); 11 | const Panel = (props) => { 12 | const { hint, children } = props; 13 | return ( 14 | 15 | {hint && ( 16 | 17 | {hint} 18 | 19 | )} 20 | {children} 21 | 22 | ); 23 | }; 24 | 25 | Panel.propTypes = { 26 | hint: PropTypes.oneOfType([ 27 | PropTypes.node, 28 | PropTypes.string, 29 | PropTypes.element, 30 | PropTypes.func, 31 | ]), 32 | children: PropTypes.oneOfType([ 33 | PropTypes.node, 34 | PropTypes.string, 35 | PropTypes.element, 36 | PropTypes.func, 37 | ]).isRequired, 38 | }; 39 | 40 | Panel.defaultProps = { 41 | hint: null, 42 | }; 43 | 44 | export default Panel; 45 | -------------------------------------------------------------------------------- /src/shared/components/LoadingOverlay/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CircularProgress } from "@material-ui/core"; 3 | import PropTypes from 'prop-types'; 4 | import styled from 'styled-components'; 5 | 6 | const Container = styled.div` 7 | position: relative; 8 | min-height: 50px; 9 | `; 10 | 11 | const Overlay = styled.div` 12 | position: absolute; 13 | left: 0; 14 | top: 0; 15 | right: 0; 16 | bottom: 0; 17 | display: flex; 18 | background: rgba(0, 0, 0, 0.1); 19 | justify-content: center; 20 | align-items: center; 21 | `; 22 | 23 | const Loading = ({ className, loading, ...rest }) => ( 24 | // eslint-disable-next-line react/forbid-component-props 25 | 26 | {loading && ( 27 | 28 | 29 | 30 | )} 31 |
32 | 33 | ); 34 | 35 | Loading.propTypes = { 36 | className: PropTypes.string, 37 | loading: PropTypes.bool, 38 | }; 39 | 40 | Loading.defaultProps = { 41 | className: '', 42 | loading: false, 43 | }; 44 | 45 | export default Loading; 46 | -------------------------------------------------------------------------------- /src/redux/api/githubGQL/getRepositories/index.js: -------------------------------------------------------------------------------- 1 | import { withCancellation } from "@/redux/utils"; 2 | import getClient from '../getClient'; 3 | import { addCursorAfter } from "../utils"; 4 | import query from './query.graphql'; 5 | 6 | /** 7 | * Gets repositories of an owner 8 | * @param {String} owner - login of a user of an organization 9 | * @param {Number} [perPage] - page size, default 10, (max is 100) 10 | * @param {String} [page] - cursor of page 11 | * @return {Promise<{rateLimit: *, data: Array, pageInfo: *}>} 12 | */ 13 | export const getRepositories = ({ owner, page = '', perPage = 10 }) => 14 | withCancellation(async (signal) => { 15 | const client = getClient(); 16 | 17 | const fixedQuery = addCursorAfter(query, page); 18 | 19 | const data = await client.graphql(fixedQuery, { 20 | owner, 21 | perPage, 22 | page, 23 | request: { 24 | signal, 25 | }, 26 | }); 27 | 28 | return { 29 | data: data?.profile?.repositories?.nodes, 30 | pageInfo: data?.profile?.repositories?.pageInfo, 31 | rateLimit: data?.rateLimit, 32 | }; 33 | }); 34 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | GitHub Visualizer 11 | 19 | 20 | 21 | 22 |
23 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/redux/api/githubGQL/getBranches/index.js: -------------------------------------------------------------------------------- 1 | import { addCursorAfter } from "@/redux/api/githubGQL/utils"; 2 | import { withCancellation } from "@/redux/utils"; 3 | import getClient from '../getClient'; 4 | import query from './query.graphql'; 5 | 6 | /** 7 | * Gets branches by owner and repo 8 | * @param {String} owner - login of a user of an organization 9 | * @param {String} repo - name of repository 10 | * @param {Number} [perPage] - page size, default 10, (max is 100) 11 | * @param {String} [page] - cursor of page 12 | * @return {Promise<{rateLimit: *, data: Array, pageInfo: *}>} 13 | */ 14 | export const getBranches = ({ owner, repo, page = '', perPage = 10 }) => 15 | withCancellation(async (signal) => { 16 | const client = getClient(); 17 | 18 | const fixedQuery = addCursorAfter(query, page); 19 | 20 | const data = await client.graphql(fixedQuery, { 21 | owner, 22 | repo, 23 | perPage, 24 | page, 25 | request: { 26 | signal, 27 | }, 28 | }); 29 | 30 | return { 31 | data: data?.repository?.refs?.nodes, 32 | pageInfo: data?.repository?.refs?.pageInfo, 33 | rateLimit: data?.rateLimit, 34 | }; 35 | }); 36 | -------------------------------------------------------------------------------- /src/shared/components/ScrollBar/index.jsx: -------------------------------------------------------------------------------- 1 | import 'react'; 2 | import Scroll from 'react-smooth-scrollbar'; 3 | import styled from 'styled-components'; 4 | 5 | // background: ${(props) => props.theme.scrollBackgroundColor}; 6 | 7 | const ScrollBar = styled(Scroll)` 8 | .scrollbar-track { 9 | background: transparent; 10 | transition: opacity 0.3s; 11 | } 12 | &:hover .scrollbar-track { 13 | opacity: 1; 14 | } 15 | 16 | .scrollbar-track-x { 17 | height: 8px; 18 | } 19 | .scrollbar-track-y { 20 | width: 8px; 21 | } 22 | 23 | .scrollbar-thumb { 24 | // TODO background scrollBackgroundColor 25 | } 26 | 27 | .scrollbar-thumb-x { 28 | height: 4px; 29 | top: 50%; 30 | margin-top: -2px; 31 | 32 | transition: height 0.3s, margin-top 0.3s; 33 | &:hover, 34 | &:active { 35 | height: 8px; 36 | margin-top: -4px; 37 | } 38 | } 39 | .scrollbar-thumb-y { 40 | width: 4px; 41 | left: 50%; 42 | margin-left: -2px; 43 | 44 | transition: width 0.3s, margin-left 0.3s; 45 | &:hover, 46 | &:active { 47 | width: 8px; 48 | margin-left: -4px; 49 | } 50 | } 51 | `; 52 | 53 | export default ScrollBar; 54 | -------------------------------------------------------------------------------- /src/themes/index.js: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from '@material-ui/core/styles'; 2 | import { createGlobalStyle } from 'styled-components'; 3 | import dark from './dark'; 4 | import light from './light'; 5 | 6 | const themes = {}; 7 | export const addTheme = (name, theme) => { 8 | themes[name] = theme; 9 | }; 10 | 11 | addTheme(light.name, light); 12 | addTheme(dark.name, dark); 13 | 14 | export const getTheme = (name) => { 15 | const theme = createMuiTheme(themes[name] || themes.dark); 16 | 17 | const GlobalStyle = createGlobalStyle` 18 | html, 19 | body { 20 | margin: 0; 21 | padding: 0; 22 | width: 100vw; 23 | height: 100vh; 24 | overflow: hidden; 25 | } 26 | 27 | //body { 28 | // font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 29 | // 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 30 | // sans-serif; 31 | // -webkit-font-smoothing: antialiased; 32 | // -moz-osx-font-smoothing: grayscale; 33 | //} 34 | // 35 | //code { 36 | // font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 37 | //} 38 | `; 39 | 40 | return { 41 | theme, 42 | GlobalStyle, 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/App/index.jsx: -------------------------------------------------------------------------------- 1 | import 'typeface-roboto'; 2 | // make sure react-hot-loader is required before react and react-dom 3 | // eslint-disable-next-line import/order 4 | import { hot } from 'react-hot-loader/root'; 5 | 6 | import React from 'react'; 7 | import { store } from '@/redux'; 8 | import AppRouter from '@/routes'; 9 | import { getTheme } from '@/themes'; 10 | import CssBaseline from '@material-ui/core/CssBaseline'; 11 | import { StylesProvider, ThemeProvider as MuiThemeProvider } from '@material-ui/core/styles'; 12 | import { Provider } from 'react-redux'; 13 | import { BrowserRouter } from 'react-router-dom'; 14 | import { ThemeProvider } from 'styled-components'; 15 | 16 | const App = () => { 17 | const { theme, GlobalStyle } = getTheme('dark'); 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default hot(App); 37 | -------------------------------------------------------------------------------- /src/redux/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { createStore, applyMiddleware } from 'redux'; 3 | import { composeWithDevTools } from 'redux-devtools-extension'; 4 | import createSagaMiddleware from 'redux-saga'; 5 | 6 | import { rootSaga } from './modules'; 7 | import { rootReducer } from './reducers'; 8 | 9 | const isDevelopment = process.env.NODE_ENV === 'development'; 10 | 11 | const sagaMiddleware = createSagaMiddleware(); 12 | 13 | const middleware = [ 14 | sagaMiddleware, 15 | ]; 16 | const composeEnhancers = composeWithDevTools({ 17 | // actionsBlacklist, 18 | }); 19 | 20 | const configure = () => { 21 | const store = createStore( 22 | rootReducer, 23 | composeEnhancers(applyMiddleware(...middleware)), 24 | ); 25 | 26 | if (isDevelopment) { 27 | window.store = store; 28 | } 29 | 30 | if (module.hot) { 31 | // Enable Webpack hot module replacement for reducers 32 | module.hot.accept('./reducers', () => { 33 | // eslint-disable-next-line global-require 34 | const nextRootReducer = require('./reducers'); 35 | store.replaceReducer(nextRootReducer); 36 | }); 37 | } 38 | 39 | sagaMiddleware.run(rootSaga); 40 | return store; 41 | }; 42 | 43 | export const store = configure(); 44 | export default configure; 45 | -------------------------------------------------------------------------------- /src/redux/api/githubGQL/getRepositories/query.graphql: -------------------------------------------------------------------------------- 1 | query($owner: String!, $perPage: Int!, ${this.cursorArgument}) { 2 | rateLimit{ 3 | cost 4 | remaining 5 | resetAt 6 | } 7 | profile: repositoryOwner(login: $owner) { 8 | repositories(first: $perPage, ${this.after}) { 9 | pageInfo { 10 | nextPage: endCursor, 11 | hasNextPage 12 | } 13 | nodes { 14 | createdAt, 15 | updatedAt, 16 | pushedAt, 17 | homepageUrl, 18 | primaryLanguage { 19 | id, 20 | name 21 | }, 22 | id, 23 | isArchived, 24 | isEmpty, 25 | isFork, 26 | isInOrganization, 27 | isLocked, 28 | isMirror, 29 | isPrivate, 30 | description, 31 | name, 32 | url, 33 | 34 | forks: forkCount, 35 | assignableUsers{ 36 | totalCount 37 | } 38 | issues { 39 | totalCount 40 | }, 41 | commitComments { 42 | totalCount 43 | }, 44 | releases { 45 | totalCount 46 | }, 47 | stargazers { 48 | totalCount 49 | }, 50 | watchers { 51 | totalCount 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/redux/utils/redux.utils.test.js: -------------------------------------------------------------------------------- 1 | import { CANCEL } from "redux-saga"; 2 | import { withCancellation } from './index'; 3 | 4 | const mock = (singal) => Promise.resolve(singal); 5 | 6 | describe('Redux Utils', () => { 7 | describe('withCancellation', () => { 8 | it('should call a passed method', () => { 9 | const fn = jest.fn(); 10 | withCancellation(fn); 11 | expect(fn).toBeCalled(); 12 | }); 13 | 14 | it('should return Promise', () => { 15 | const fn = jest.fn(mock); 16 | const result = withCancellation(fn); 17 | expect(result).toBeInstanceOf(Promise); 18 | }); 19 | 20 | it('should call a method with passed signal', async () => { 21 | const fn = jest.fn(mock); 22 | const result = await withCancellation(fn); 23 | expect(result).toBeInstanceOf(AbortSignal); 24 | }); 25 | 26 | it('should add CANCEL props to returned object', async () => { 27 | const fn = jest.fn(mock); 28 | const result = withCancellation(fn); 29 | expect(result[CANCEL]).toBeDefined(); 30 | expect(result[CANCEL]).toBeInstanceOf(Function); 31 | result[CANCEL](); 32 | const signal = await result; 33 | expect(signal).toHaveProperty('aborted', true); 34 | }); 35 | }); 36 | 37 | // TODO createSlice test 38 | }); 39 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFilename = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFilename}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "roots": [ 3 | "/src" 4 | ], 5 | "collectCoverageFrom": [ 6 | "src/**/*.{js,jsx,ts,tsx}", 7 | "!src/**/*.d.ts" 8 | ], 9 | "setupFiles": [ 10 | "react-app-polyfill/jsdom" 11 | ], 12 | "setupFilesAfterEnv": [ 13 | "/src/setupTests.js" 14 | ], 15 | "testMatch": [ 16 | "/src/**/__tests__/**/*.{js,jsx,ts,tsx}", 17 | "/src/**/*.{spec,test}.{js,jsx,ts,tsx}" 18 | ], 19 | "testEnvironment": "jest-environment-jsdom-fourteen", 20 | "transform": { 21 | "^.*\\.graphql$": "jest-raw-loader", 22 | "^.+\\.(js|jsx|ts|tsx)$": "/node_modules/babel-jest", 23 | "^.+\\.css$": "/config/jest/cssTransform.js", 24 | "^(?!.*\\.(js|jsx|ts|tsx|css|json|graphql)$)": "/config/jest/fileTransform.js" 25 | }, 26 | "transformIgnorePatterns": [ 27 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$", 28 | "^.+\\.module\\.(css|sass|scss)$" 29 | ], 30 | "modulePaths": [], 31 | "moduleNameMapper": { 32 | "^react-native$": "react-native-web", 33 | "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy", 34 | "^@/(.*)$": "/src/$1" 35 | }, 36 | "moduleFileExtensions": [ 37 | "web.js", 38 | "js", 39 | "web.ts", 40 | "ts", 41 | "web.tsx", 42 | "tsx", 43 | "json", 44 | "web.jsx", 45 | "jsx", 46 | "node" 47 | ], 48 | "watchPlugins": [ 49 | "jest-watch-typeahead/filename", 50 | "jest-watch-typeahead/testname" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI or explicitly running all tests 42 | if ( 43 | !process.env.CI && 44 | argv.indexOf('--watchAll') === -1 && 45 | argv.indexOf('--watchAll=false') === -1 46 | ) { 47 | // https://github.com/facebook/create-react-app/issues/5210 48 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 49 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 50 | } 51 | 52 | 53 | jest.run(argv); 54 | -------------------------------------------------------------------------------- /src/redux/api/github/getCommits.js: -------------------------------------------------------------------------------- 1 | import { parsePageInfo, parseRateLimit } from "@/redux/api/github/utils"; 2 | import { withCancellation } from "@/redux/utils"; 3 | import getClient from './getClient'; 4 | 5 | /** 6 | * Gets commits from a repo's branch of a user 7 | * @param {String} owner - login of a user of an organization 8 | * @param {String} repo - name of repository 9 | * @param {String} branch - name of branch 10 | * @param {Number} [perPage] - page size, default 10, (max is 100) 11 | * @param {Number} [page] - index of page 12 | * @return {Promise<{rateLimit: *, data: Array, pageInfo: *}>} 13 | */ 14 | export const getCommits = ({ owner, repo, branch, perPage = 10, page }) => 15 | withCancellation(async (signal) => { 16 | const client = getClient(); 17 | 18 | const data = await client.repos.listCommits({ 19 | repo, 20 | owner, 21 | sha: branch, 22 | per_page: perPage, 23 | page, 24 | request: { 25 | signal, 26 | }, 27 | }); 28 | 29 | const ids = data?.data?.map(({ sha }) => sha) || []; 30 | 31 | const commits = await Promise.all(ids.map(async (ref) => { 32 | const d = await client.repos.getCommit({ 33 | owner, 34 | repo, 35 | ref, 36 | request: { 37 | signal, 38 | }, 39 | }); 40 | return [d?.data, d?.headers]; 41 | })); 42 | 43 | const [[,rateHeaders]] = commits.slice(-1); 44 | 45 | return { 46 | data: commits.map(([item]) => item), 47 | pageInfo: parsePageInfo(data?.headers), 48 | rateLimit: parseRateLimit(rateHeaders), 49 | }; 50 | }); 51 | -------------------------------------------------------------------------------- /old/js/cookies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * script from http://learn.javascript.ru/cookie 3 | */ 4 | // возвращает cookie с именем name, если есть, если нет, то undefined 5 | function getCookie(name) { 6 | var matches = document.cookie.match(new RegExp( 7 | "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)" 8 | )); 9 | return matches ? JSON.parse(decodeURIComponent(matches[1])) : undefined; 10 | } 11 | 12 | // устанавливает cookie c именем name и значением value 13 | // options - объект с свойствами cookie (expires, path, domain, secure) 14 | function setCookie(name, value, options) { 15 | options = options || {}; 16 | 17 | var expires = options.expires; 18 | 19 | if (typeof expires == "number" && expires) { 20 | var d = new Date(); 21 | d.setTime(d.getTime() + expires * 1000); 22 | expires = options.expires = d; 23 | } 24 | if (expires && expires.toUTCString) { 25 | options.expires = expires.toUTCString(); 26 | } 27 | 28 | value = encodeURIComponent(JSON.stringify(value)); 29 | 30 | var updatedCookie = name + "=" + value; 31 | 32 | for (var propName in options) { 33 | if (!options.hasOwnProperty(propName)) 34 | continue; 35 | 36 | updatedCookie += "; " + propName; 37 | var propValue = options[propName]; 38 | if (propValue !== true) { 39 | updatedCookie += "=" + propValue; 40 | } 41 | } 42 | 43 | document.cookie = updatedCookie; 44 | } 45 | 46 | // удаляет cookie с именем name 47 | function deleteCookie(name) { 48 | setCookie(name, "", { expires: -1 }) 49 | } 50 | /**end script*/ -------------------------------------------------------------------------------- /src/redux/api/githubGQL/getCommits/index.js: -------------------------------------------------------------------------------- 1 | import { withCancellation } from "@/redux/utils"; 2 | import getClient from '../getClient'; 3 | import { addCursorAfter } from "../utils"; 4 | import query from './query.graphql'; 5 | 6 | /** 7 | * Gets commits from a repo's branch of a user 8 | * @param {String} owner - login of a user of an organization 9 | * @param {String} repo - name of repository 10 | * @param {String} branch - name of branch 11 | * @param {Number} [perPage] - page size, default 10, (max is 100) 12 | * @param {String} [page] - cursor of page 13 | * @return {Promise<{rateLimit: *, data: Array, pageInfo: *}>} 14 | */ 15 | export const getCommits = ({ owner, repo, branch, page = '', perPage = 10 }) => 16 | withCancellation(async (signal) => { 17 | const client = getClient(); 18 | 19 | const fixedQuery = addCursorAfter(query, page); 20 | 21 | const data = await client.graphql(fixedQuery, { 22 | owner, 23 | repo, 24 | perPage, 25 | branch, 26 | page, 27 | request: { 28 | signal, 29 | }, 30 | }); 31 | 32 | const ids = data?.repository?.ref?.target?.history?.nodes?.map(({ oid }) => oid) || []; 33 | 34 | const commits = await Promise.all(ids.map(async (ref) => { 35 | const d = await client.repos.getCommit({ 36 | owner, 37 | repo, 38 | ref, 39 | request: { 40 | signal, 41 | }, 42 | }); 43 | return d?.data; 44 | })); 45 | 46 | return { 47 | data: commits, 48 | pageInfo: data?.repository?.ref?.target?.history?.pageInfo, 49 | rateLimit: data?.rateLimit, 50 | }; 51 | }); 52 | -------------------------------------------------------------------------------- /src/components/Header/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import FetchTopUser from "@/components/Header/FetchTopUser"; 3 | import User from "@/components/Header/User"; 4 | import UserSearch from "@/components/Header/UserSearch"; 5 | import Collapse from "@material-ui/core/Collapse"; 6 | import Grid from "@material-ui/core/Grid"; 7 | import Paper from "@material-ui/core/Paper"; 8 | import { withStyles } from "@material-ui/core/styles"; 9 | import Tab from "@material-ui/core/Tab"; 10 | import Tabs from "@material-ui/core/Tabs"; 11 | 12 | 13 | const PaperStyled = withStyles(() => ({ 14 | root: { 15 | position: 'absolute', 16 | left: '50%', 17 | top: 0, 18 | transform: 'translate(-50%, 0)', 19 | }, 20 | }))(Paper); 21 | 22 | const Header = () => { 23 | const [value, setValue] = useState(0); 24 | 25 | const handleChange = (event, newValue) => { 26 | setValue(newValue); 27 | }; 28 | 29 | return ( 30 | 31 | 32 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | Repository 48 |
49 |
50 | Show 51 |
52 |
53 | 54 | 55 | 56 |
57 | ); 58 | }; 59 | 60 | export default Header; 61 | -------------------------------------------------------------------------------- /src/components/Header/StepsBar/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import Step from '@material-ui/core/Step'; 3 | import StepLabel from '@material-ui/core/StepLabel'; 4 | import Stepper from '@material-ui/core/Stepper'; 5 | import styled from 'styled-components'; 6 | import StepRepo from './StepRepo'; 7 | import StepShow from './StepShow'; 8 | import StepUser from './StepUser'; 9 | 10 | const steps = [{ 11 | key: 'user', 12 | component: StepUser, 13 | }, { 14 | key: 'repo', 15 | component: StepRepo, 16 | }, { 17 | key: 'show', 18 | component: StepShow, 19 | }]; 20 | 21 | const StepperStyled = styled(Stepper)` 22 | padding: 0; 23 | `; 24 | 25 | const Index = () => { 26 | const [opened, setOpened] = useState(-1); 27 | const [active, setActive] = useState(0); 28 | 29 | const onOpenBy = useCallback( 30 | (index) => () => { 31 | if (index === opened) { 32 | return; 33 | } 34 | 35 | const nextOpened = opened !== index ? index : -1; 36 | setOpened(nextOpened); 37 | setActive(nextOpened >= 0 ? nextOpened : active); 38 | }, 39 | [opened, active], 40 | ); 41 | 42 | const onCloseBy = useCallback( 43 | () => { 44 | setOpened(-1); 45 | }, 46 | [], 47 | ); 48 | 49 | return ( 50 | 51 | {steps.map(({ key, component: Component }, index) => ( 52 | 57 | 58 | 59 | 60 | 61 | ))} 62 | 63 | ); 64 | }; 65 | 66 | export default Index; 67 | -------------------------------------------------------------------------------- /src/redux/api/github/getBranches.js: -------------------------------------------------------------------------------- 1 | import { withCancellation } from "@/redux/utils"; 2 | import getClient from './getClient'; 3 | import { parsePageInfo, parseRateLimit } from "./utils"; 4 | 5 | const reg = /page=(\d+)>; rel="last"/; 6 | const getCount = (link, defValue = 0) => 7 | Boolean(link && link.includes('rel="last"')) 8 | ? +link.match(reg)[1] 9 | : defValue; 10 | 11 | /** 12 | * Gets branches by owner and repo 13 | * @param {String} owner - login of a user of an organization 14 | * @param {String} repo - name of repository 15 | * @param {Number} [perPage] - page size, default 10, (max is 100) 16 | * @param {Number} [page] - index of page 17 | * @return {Promise<{rateLimit: *, data: Array, pageInfo: *}>} 18 | */ 19 | export const getBranches = ({ owner, repo, perPage = 10, page }) => 20 | withCancellation(async (signal) => { 21 | const client = getClient(); 22 | 23 | const data = await client.repos.listBranches({ 24 | owner, 25 | repo, 26 | per_page: perPage, 27 | page, 28 | request: { 29 | signal, 30 | }, 31 | }); 32 | 33 | const branches = await Promise.all((data?.data || []).map(async (branch) => { 34 | const commits = await client.repos.listCommits({ 35 | repo, 36 | owner, 37 | sha: branch.name, 38 | per_page: 1, 39 | request: { 40 | signal, 41 | }, 42 | }); 43 | 44 | return [ 45 | { 46 | ...branch, 47 | commits: getCount(commits?.headers?.link, commits?.data?.length), 48 | }, 49 | commits?.headers, 50 | ]; 51 | })); 52 | 53 | const [[,rateHeaders]] = branches.slice(-1); 54 | 55 | return { 56 | data: branches.map(([item]) => item), 57 | pageInfo: parsePageInfo(data?.headers), 58 | rateLimit: parseRateLimit(rateHeaders), 59 | }; 60 | }); 61 | -------------------------------------------------------------------------------- /config/getHttpsConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const crypto = require('crypto'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const paths = require('./paths'); 8 | 9 | // Ensure the certificate and key provided are valid and if not 10 | // throw an easy to debug error 11 | function validateKeyAndCerts({ cert, key, keyFile, crtFile }) { 12 | let encrypted; 13 | try { 14 | // publicEncrypt will throw an error with an invalid cert 15 | encrypted = crypto.publicEncrypt(cert, Buffer.from('test')); 16 | } catch (err) { 17 | throw new Error( 18 | `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}` 19 | ); 20 | } 21 | 22 | try { 23 | // privateDecrypt will throw an error with an invalid key 24 | crypto.privateDecrypt(key, encrypted); 25 | } catch (err) { 26 | throw new Error( 27 | `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${ 28 | err.message 29 | }` 30 | ); 31 | } 32 | } 33 | 34 | // Read file and throw an error if it doesn't exist 35 | function readEnvFile(file, type) { 36 | if (!fs.existsSync(file)) { 37 | throw new Error( 38 | `You specified ${chalk.cyan( 39 | type 40 | )} in your env, but the file "${chalk.yellow(file)}" can't be found.` 41 | ); 42 | } 43 | return fs.readFileSync(file); 44 | } 45 | 46 | // Get the https config 47 | // Return cert files if provided in env, otherwise just true or false 48 | function getHttpsConfig() { 49 | const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env; 50 | const isHttps = HTTPS === 'true'; 51 | 52 | if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) { 53 | const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE); 54 | const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE); 55 | const config = { 56 | cert: readEnvFile(crtFile, 'SSL_CRT_FILE'), 57 | key: readEnvFile(keyFile, 'SSL_KEY_FILE'), 58 | }; 59 | 60 | validateKeyAndCerts({ ...config, keyFile, crtFile }); 61 | return config; 62 | } 63 | return isHttps; 64 | } 65 | 66 | module.exports = getHttpsConfig; 67 | -------------------------------------------------------------------------------- /src/redux/api/githubGQL/api.githubGQL.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | searchAccount, 3 | getProfile, 4 | getRepositories, 5 | getBranches, 6 | getCommits, 7 | } from './index'; 8 | 9 | describe('Github GraphQL API', () => { 10 | it('should search accounts', async () => { 11 | expect(searchAccount).toBeInstanceOf(Function); 12 | const data = await searchAccount('test ggg'); 13 | expect(data).toBeDefined(); 14 | expect(data).toHaveProperty('data'); 15 | expect(Array.isArray(data.data)).toBe(true); 16 | }); 17 | 18 | it('should get profile of user by login', async () => { 19 | expect(getProfile).toBeInstanceOf(Function); 20 | const login = 'artzub'; 21 | const data = await getProfile(login); 22 | expect(data).toHaveProperty('data.login', login); 23 | }); 24 | 25 | it('should get profile of organisation by login', async () => { 26 | expect(getProfile).toBeInstanceOf(Function); 27 | const login = 'github'; 28 | const data = await getProfile(login, true); 29 | expect(data).toHaveProperty('data.login', login); 30 | }); 31 | 32 | it('should get repositories of a profile', async () => { 33 | expect(getRepositories).toBeInstanceOf(Function); 34 | const owner = 'ossf'; 35 | const data = await getRepositories({ owner, perPage: 1 }); 36 | expect(data).toHaveProperty('data'); 37 | expect(Array.isArray(data.data)).toBe(true); 38 | }); 39 | 40 | it('should get branches of a repo', async () => { 41 | expect(getBranches).toBeInstanceOf(Function); 42 | const owner = 'd3'; 43 | const repo = 'd3'; 44 | const data = await getBranches({ owner, repo, perPage: 1 }); 45 | expect(data).toHaveProperty('data'); 46 | expect(Array.isArray(data.data)).toBe(true); 47 | }); 48 | 49 | it('should get commits of a repo and a branch', async () => { 50 | expect(getCommits).toBeInstanceOf(Function); 51 | const owner = 'd3'; 52 | const repo = 'd3'; 53 | const branch = '4'; 54 | const data = await getCommits({ owner, repo, branch, perPage: 1 }); 55 | expect(data).toHaveProperty('data'); 56 | expect(Array.isArray(data.data)).toBe(true); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/redux/api/github/api.github.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | searchAccount, 3 | getProfile, 4 | getRepositories, 5 | getBranches, 6 | getCommits, 7 | } from './index'; 8 | import { CANCEL } from "redux-saga"; 9 | 10 | describe('Github Rest API', () => { 11 | it('should search accounts', async () => { 12 | expect(searchAccount).toBeInstanceOf(Function); 13 | const data = await searchAccount('test g'); 14 | expect(data).toBeDefined(); 15 | expect(data).toHaveProperty('data'); 16 | expect(Array.isArray(data.data)).toBe(true); 17 | }); 18 | 19 | it('should get profile of user by login', async () => { 20 | expect(getProfile).toBeInstanceOf(Function); 21 | const login = 'artzub'; 22 | const data = await getProfile(login); 23 | expect(data).toHaveProperty('data.login', login); 24 | }); 25 | 26 | it('should get profile of organisation by login', async () => { 27 | expect(getProfile).toBeInstanceOf(Function); 28 | const login = 'github'; 29 | const data = await getProfile(login); 30 | expect(data).toHaveProperty('data.login', login); 31 | }); 32 | 33 | it('should get repositories of a profile', async () => { 34 | expect(getRepositories).toBeInstanceOf(Function); 35 | const owner = 'ossf'; 36 | const data = await getRepositories({ owner, perPage: 1 }); 37 | expect(data).toHaveProperty('data'); 38 | expect(Array.isArray(data.data)).toBe(true); 39 | }); 40 | 41 | it('should get branches of a repo', async () => { 42 | expect(getBranches).toBeInstanceOf(Function); 43 | const owner = 'd3'; 44 | const repo = 'd3'; 45 | const data = await getBranches({ owner, repo, perPage: 1 }); 46 | expect(data).toHaveProperty('data'); 47 | expect(Array.isArray(data.data)).toBe(true); 48 | }); 49 | 50 | it('should get commits of a repo and a branch', async () => { 51 | expect(getCommits).toBeInstanceOf(Function); 52 | const owner = 'd3'; 53 | const repo = 'd3'; 54 | const branch = '4'; 55 | const data = await getCommits({ owner, repo, branch, perPage: 1 }); 56 | expect(data).toHaveProperty('data'); 57 | expect(Array.isArray(data.data)).toBe(true); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/components/Header/UserBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import IconButton from '@material-ui/core/IconButton'; 3 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 4 | import ListItemText from '@material-ui/core/ListItemText'; 5 | import Menu from '@material-ui/core/Menu'; 6 | import MenuItem from '@material-ui/core/MenuItem'; 7 | import AccountCircle from '@material-ui/icons/AccountCircle'; 8 | import ExitToAppIcon from '@material-ui/icons/ExitToApp'; 9 | import GitHub from '@material-ui/icons/GitHub'; 10 | import { Link } from 'react-router-dom'; 11 | import styled from 'styled-components'; 12 | 13 | const Container = styled.div` 14 | `; 15 | 16 | const UserBar = () => { 17 | const [anchor, setAnchor] = useState(null); 18 | const opened = Boolean(anchor); 19 | 20 | const handleMenu = useCallback( 21 | ({ currentTarget }) => setAnchor(currentTarget), 22 | [], 23 | ); 24 | 25 | const handleClose = useCallback( 26 | () => setAnchor(null), 27 | [], 28 | ); 29 | 30 | return ( 31 | 32 | 39 | 40 | 41 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | ); 70 | }; 71 | 72 | export default UserBar; 73 | -------------------------------------------------------------------------------- /src/redux/modules/profiles.js: -------------------------------------------------------------------------------- 1 | import { getProfile, searchAccount } from '@/redux/api/github'; 2 | import { createSlice, startFetching, stopFetching } from "@/redux/utils"; 3 | import { call, put } from 'redux-saga/effects'; 4 | 5 | const initialState = { 6 | isFetching: false, 7 | profile: null, 8 | searched: [], 9 | top: [], 10 | error: null, 11 | }; 12 | 13 | const setProfile = (state, profile) => { 14 | state.profile = profile; 15 | }; 16 | 17 | export default createSlice({ 18 | name: 'profiles', 19 | initialState, 20 | reducers: { 21 | fetchProfile: startFetching, 22 | fetchProfileSuccess: (state, { payload }) => { 23 | stopFetching(state); 24 | setProfile(state, payload); 25 | }, 26 | 27 | setProfile: (state, { payload }) => { 28 | setProfile(state, payload); 29 | }, 30 | 31 | search: startFetching, 32 | searchSuccess: (state, { payload }) => { 33 | stopFetching(state); 34 | state.searched = Array.isArray(payload) ? payload : []; 35 | }, 36 | fetchTop: startFetching, 37 | fetchTopSuccess: (state, { payload }) => { 38 | stopFetching(state); 39 | state.top = Array.isArray(payload) ? payload : []; 40 | }, 41 | 42 | fail: (state, { payload: { message } }) => { 43 | stopFetching(state); 44 | state.error = message; 45 | }, 46 | }, 47 | 48 | sagas: (actions) => ({ 49 | [actions.fetchProfile]: { 50 | * saga({ payload }) { 51 | try { 52 | const { data } = yield call(getProfile, payload); 53 | yield put(actions.fetchProfileSuccess(data)); 54 | } catch (error) { 55 | yield put(actions.fail(error)); 56 | } 57 | }, 58 | }, 59 | 60 | [actions.search]: { 61 | * saga({ payload }) { 62 | try { 63 | const { data } = yield call(searchAccount, payload); 64 | yield put(actions.searchSuccess(data)); 65 | } catch (error) { 66 | yield put(actions.fail(error)); 67 | } 68 | }, 69 | }, 70 | 71 | [actions.fetchTop]: { 72 | * saga() { 73 | try { 74 | const { data } = yield call(searchAccount, 'followers:>1000'); 75 | yield put(actions.fetchTopSuccess(data)); 76 | } catch (error) { 77 | yield put(actions.fail(error)); 78 | } 79 | }, 80 | }, 81 | }), 82 | 83 | selectors: (selector) => ({ 84 | 85 | }), 86 | }); 87 | -------------------------------------------------------------------------------- /src/components/Header/StepsBar/StepUser.jsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useCallback, 3 | useMemo, 4 | useState, 5 | } from 'react'; 6 | import Checkbox from '@material-ui/core/Checkbox'; 7 | import FormControl from '@material-ui/core/FormControl'; 8 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 9 | import FormGroup from '@material-ui/core/FormGroup'; 10 | import FormLabel from '@material-ui/core/FormLabel'; 11 | import IconButton from '@material-ui/core/IconButton'; 12 | import InputBase from '@material-ui/core/InputBase'; 13 | import SearchIcon from '@material-ui/icons/Search'; 14 | import Panel from './Panel'; 15 | import Step, { propTypes } from './Step'; 16 | 17 | const StepUser = (props) => { 18 | const { onClickAway, open } = props; 19 | 20 | const [repos, setRepos] = useState(true); 21 | const [histogram, setHistogram] = useState(true); 22 | 23 | const onReposChange = useCallback( 24 | ({ target }) => setRepos(target.checked), 25 | [], 26 | ); 27 | 28 | const onHistogramChange = useCallback( 29 | ({ target }) => setHistogram(target.checked), 30 | [], 31 | ); 32 | 33 | const title = useMemo( 34 | () => ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | ), 42 | [], 43 | ); 44 | 45 | return ( 46 | 47 | 48 | 49 | 50 | Display: 51 | 52 | 53 | } 55 | label="Layer repos" 56 | /> 57 | } 59 | label="Layer histogram languages" 60 | /> 61 | 62 | 63 | 64 | 65 | ); 66 | }; 67 | 68 | const { 69 | title, 70 | children, 71 | ...stepProps 72 | } = propTypes; 73 | 74 | StepUser.propTypes = { 75 | ...stepProps, 76 | }; 77 | 78 | StepUser.defaultProps = { 79 | open: false, 80 | }; 81 | 82 | export default StepUser; 83 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 13 | // "public path" at which the app is served. 14 | // webpack needs to know it to put the right