├── 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 |
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