├── .eslintrc.json ├── .github └── workflows │ └── integrate.yaml ├── .gitignore ├── .npmrc ├── .prettierrc ├── LICENSE ├── README.md ├── __mocks__ ├── fileMock.js └── gh-polyglot.js ├── babel.config.js ├── cypress.config.ts ├── cypress ├── .eslintrc.json ├── e2e │ └── app.cy.js ├── fixtures │ └── example.json └── support │ ├── commands.ts │ └── e2e.ts ├── jest.config.ts ├── netlify.toml ├── package-lock.json ├── package.json ├── public ├── _redirects ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── setupTest.ts ├── src ├── api │ ├── base.js │ └── githubAPI.js ├── assets │ ├── demo.gif │ ├── loader.gif │ └── logo.png ├── components │ ├── Activities.js │ ├── App.js │ ├── Button.js │ ├── Chart.js │ ├── Error.js │ ├── Footer.js │ ├── Form.js │ ├── Header.js │ ├── Loader.js │ ├── Logo.js │ ├── MaterialTabs.js │ ├── Profile.js │ ├── Stats │ │ ├── Stats.js │ │ ├── __tests__ │ │ │ └── utils.test.ts │ │ ├── index.ts │ │ └── utils.ts │ ├── Timeline.js │ ├── TimelineItem.js │ ├── Toggle.js │ ├── __tests__ │ │ └── Form.test.tsx │ └── index.js ├── contexts │ ├── LanguageContext.js │ └── ThemeProvider.js ├── index.tsx ├── mock.data.ts ├── mocks │ ├── handlers.ts │ └── server.ts ├── pages │ ├── Home.js │ ├── UserProfile.js │ └── __tests__ │ │ └── UserProfile.test.tsx ├── style │ ├── GlobalStyle.js │ ├── dark.js │ ├── index.js │ └── light.js ├── types.ts └── useDarkMode.js ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "jest": true 6 | }, 7 | 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:react/recommended", 12 | "plugin:prettier/recommended" 13 | ], 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaVersion": "latest", 17 | "sourceType": "module" 18 | }, 19 | "plugins": ["@typescript-eslint", "react"], 20 | "rules": { 21 | "strict": ["error", "never"], 22 | /* TODO: once prop types are added remove this rule */ 23 | "react/prop-types": "off", 24 | "@typescript-eslint/no-explicit-any": "warn" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/integrate.yaml: -------------------------------------------------------------------------------- 1 | # workflow name 2 | name: Gitpedia Continuous Integration 3 | 4 | # when should workflow run 5 | on: 6 | pull_request: 7 | branches: [master] 8 | 9 | # each workflow has one or more jobs 10 | jobs: 11 | test-pull-request: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | - name: Setup node 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 18 20 | - run: npm ci 21 | - run: npm test 22 | - run: npm run build 23 | - name: Run cypress tests 24 | uses: cypress-io/github-action@v5 25 | with: 26 | start: npm start 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | cypress/videos 11 | cypress/screenshots 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # Local Netlify folder 28 | .netlify -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # TODO: remove once peer deps error are solved 2 | legacy-peer-deps=true -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "trailingComma": "none", 7 | "jsxBracketSameLine": true 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Khusharth A Patani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | gitpedia 3 |
4 |
5 | Netlify Status 6 | 7 |

8 | 9 | A web application to :mag: view a github's user profile in a more simple and beautiful way. 10 | 11 | ## :film_projector: DEMO 12 | 13 |

14 | musology 15 |

16 | 17 | ## :man_technologist: Technology Stack 18 | 19 | ![react](https://img.shields.io/badge/frontend-react-61dafb?style=flat&logo=React) 20 | ![styled-components](https://img.shields.io/badge/styling-styled--components-%23DB7093?style=flat&logo=styled-components) 21 | ![react-chart-js-2](https://img.shields.io/badge/charts-react--chart--js--2-yellow?style=flat&logo=Deezer) 22 | ![icons](https://img.shields.io/badge/icons-react--icons-red?style=flat&logo=React) 23 | 24 | - [React](https://reactjs.org/) 25 | - [styled-components](https://styled-components.com/) 26 | - [React Chart Js 2](https://www.npmjs.com/package/react-chartjs-2) 27 | - [React Icons](https://react-icons.github.io/react-icons/) 28 | 29 | ### API used 30 | 31 | - For fetching github's user data : [Github API](https://developer.github.com/v3/) 32 | 33 | ## :hatching_chick: Prerequisites 34 | 35 | - [node](https://nodejs.org/en/) >= 12.18.0 36 | - npm >= 6.14.4 37 | 38 | ## :zap: Installation 39 | 40 | 1. Clone / Download [this](https://github.com/khusharth/gitpedia) repo. 41 | 2. Inside the project open a terminal and run: 42 | ``` 43 | npm install 44 | ``` 45 | This will install all the project dependencies. 46 | 3. Create a **.env** file in the project root folder and add the following: 47 | 48 | ``` 49 | REACT_APP_GITHUB_CLIENT_ID = yourClientId 50 | REACT_APP_GITHUB_CLIENT_SECRET = yourSecretKey 51 | ``` 52 | 53 | Replace yourClientId and yourSecretKey with your own **Client and Secret Key** . 54 | 55 | > Get your Client Id and Secret by signing in to your github account and then go to your setting -> developer setting -> OAuth Apps -> New OAuth App 56 | 57 | 4. To start the development server run: 58 | ``` 59 | npm start 60 | ``` 61 | 62 | ## :man_in_tuxedo: Author 63 | 64 | [![Twitter](https://img.shields.io/badge/follow-%40khusharth19-1DA1F2?style=flat&logo=Twitter)](https://twitter.com/khusharth19) 65 | 66 | [![LinkedIn](https://img.shields.io/badge/connect-%40khusharthpatani-%230077B5?style=flat&logo=LinkedIn)](https://www.linkedin.com/in/khusharth/) 67 | 68 | ## :page_with_curl: Licence 69 | 70 | [MIT License](https://github.com/khusharth/gitpedia/blob/master/LICENSE) Copyright (c) 2020 Khusharth A Patani 71 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = 'test-file-stub'; 3 | -------------------------------------------------------------------------------- /__mocks__/gh-polyglot.js: -------------------------------------------------------------------------------- 1 | export default class GhPolyglot { 2 | constructor() { 3 | // console.log('GhPolyglot: constructor was called'); 4 | } 5 | 6 | userStats(func) { 7 | func('', defaultStats); 8 | } 9 | } 10 | 11 | const defaultStats = [ 12 | { 13 | label: 'JavaScript', 14 | value: 19, 15 | color: '#f1e05a' 16 | }, 17 | { 18 | label: 'TypeScript', 19 | value: 9, 20 | color: '#3178c6' 21 | }, 22 | { 23 | label: 'Python', 24 | value: 3, 25 | color: '#3572A5' 26 | }, 27 | { 28 | label: 'Others', 29 | value: 2, 30 | color: '#ccc' 31 | }, 32 | { 33 | label: 'Dart', 34 | value: 2, 35 | color: '#00B4AB' 36 | }, 37 | { 38 | label: 'Objective-C', 39 | value: 1, 40 | color: '#438eff' 41 | }, 42 | { 43 | label: 'Svelte', 44 | value: 1, 45 | color: '#ff3e00' 46 | }, 47 | { 48 | label: 'MDX', 49 | value: 1, 50 | color: '#fcb32c' 51 | } 52 | ]; 53 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = { 3 | presets: [ 4 | ['@babel/preset-env', { targets: { node: 'current' } }], 5 | '@babel/preset-react', 6 | '@babel/preset-typescript' 7 | ] 8 | }; 9 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | setupNodeEvents(on, config) { 6 | // implement node event listeners here 7 | config.baseUrl = 'http://localhost:3000'; 8 | } 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /cypress/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "plugins": ["eslint-plugin-cypress"], 4 | "extends": ["plugin:cypress/recommended"], 5 | "env": {"cypress/global": true} 6 | } -------------------------------------------------------------------------------- /cypress/e2e/app.cy.js: -------------------------------------------------------------------------------- 1 | describe('app test', () => { 2 | it('first test', () => { 3 | cy.visit('http://localhost:3000'); 4 | 5 | cy.get('.sc-fzpans').click(); 6 | }); 7 | }); 8 | 9 | describe('app test 2', () => { 10 | it('2nd test', () => { 11 | cy.visit('http://localhost:3000'); 12 | 13 | cy.findByPlaceholderText(/^Enter Github Username$/) 14 | .click() 15 | .type('khusharth'); 16 | 17 | cy.findByLabelText('search').click(); 18 | 19 | cy.findByRole('tab', { name: 'Timeline' }).click(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } 38 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | import '@testing-library/cypress/add-commands'; 22 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | // enable dom apis for tests 5 | testEnvironment: 'jsdom', 6 | 7 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 8 | setupFilesAfterEnv: ['./setupTest.ts'], 9 | 10 | moduleDirectories: ['node_modules', 'src'], 11 | 12 | moduleNameMapper: { 13 | // Handle image imports 14 | // https://jestjs.io/docs/webpack#handling-static-assets 15 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 16 | '/__mocks__/fileMock.js', 17 | 18 | // Handle module aliases 19 | '^src/(.*)$': '/src/$1' 20 | }, 21 | 22 | // coverage 23 | collectCoverageFrom: ['/src/**/*.{js,ts,tsx}'], 24 | coveragePathIgnorePatterns: ['/node_modules/', '/src/style'] 25 | }; 26 | 27 | export default config; 28 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish="build" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitpedia", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.6.1", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/user-event": "^7.1.2", 9 | "@types/jest": "^29.5.3", 10 | "@types/node": "^20.4.4", 11 | "@types/react": "^18.2.15", 12 | "@types/react-dom": "^18.2.7", 13 | "@types/react-router-dom": "^5.3.3", 14 | "axios": "^1.4.0", 15 | "chart.js": "^2.9.3", 16 | "gh-polyglot": "^2.3.2", 17 | "react": "^17.0.2", 18 | "react-chartjs-2": "^2.9.0", 19 | "react-dom": "^17.0.2", 20 | "react-icons": "^3.10.0", 21 | "react-router-dom": "^5.2.0", 22 | "react-scripts": "^5.0.1", 23 | "styled-components": "^5.1.1", 24 | "typescript": "^5.1.6" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "eject": "react-scripts eject", 30 | "lint": "eslint --ignore-path .gitignore .", 31 | "test": "jest", 32 | "test:watch": "jest --watch --detectOpenHandles", 33 | "test:coverage": "jest --coverage" 34 | }, 35 | "eslintConfig": { 36 | "extends": "react-app" 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | }, 50 | "devDependencies": { 51 | "@babel/core": "^7.22.9", 52 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11", 53 | "@babel/preset-env": "^7.22.9", 54 | "@babel/preset-react": "^7.22.5", 55 | "@babel/preset-typescript": "^7.22.5", 56 | "@testing-library/cypress": "^9.0.0", 57 | "@testing-library/dom": "^9.3.1", 58 | "@testing-library/react": "^12.1.5", 59 | "@types/styled-components": "^5.1.26", 60 | "@types/testing-library__react": "^10.2.0", 61 | "@typescript-eslint/eslint-plugin": "^6.1.0", 62 | "@typescript-eslint/parser": "^6.1.0", 63 | "babel-jest": "^29.6.1", 64 | "cypress": "^12.17.2", 65 | "eslint": "^8.45.0", 66 | "eslint-config-prettier": "^8.8.0", 67 | "eslint-plugin-cypress": "^2.13.3", 68 | "eslint-plugin-prettier": "^5.0.0", 69 | "eslint-plugin-react": "^7.33.0", 70 | "jest": "^29.6.1", 71 | "jest-canvas-mock": "^2.5.2", 72 | "jest-environment-jsdom": "^29.6.1", 73 | "msw": "^1.2.3", 74 | "prettier": "^3.0.0", 75 | "ts-node": "^10.9.1" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khusharth/gitpedia/53f6ca8cf9296f7927f76be73ab1b1ea6437b190/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 18 | 19 | 28 | 29 | 30 | 39 | 40 | GitPedia 41 | 42 | 43 | 44 | 45 |
46 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khusharth/gitpedia/53f6ca8cf9296f7927f76be73ab1b1ea6437b190/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khusharth/gitpedia/53f6ca8cf9296f7927f76be73ab1b1ea6437b190/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /setupTest.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | import 'jest-canvas-mock'; 3 | -------------------------------------------------------------------------------- /src/api/base.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import axios from 'axios'; 3 | 4 | export const BASE_URL = 'https://api.github.com'; 5 | 6 | let githubClientId; 7 | let githubClientSecret; 8 | 9 | if (process.env.NODE_ENV !== 'production') { 10 | // Local Environment Variables from .env.local 11 | githubClientId = process.env.REACT_APP_GITHUB_CLIENT_ID; 12 | githubClientSecret = process.env.REACT_APP_GITHUB_CLIENT_SECRET; 13 | } else { 14 | // Netlify Environment Variables 15 | githubClientId = process.env.GITHUB_CLIENT_ID; 16 | githubClientSecret = process.env.GITHUB_CLIENT_SECRET; 17 | } 18 | 19 | // A pre configured instace of axios for github API 20 | export default axios.create({ 21 | baseURL: BASE_URL, 22 | auth: { 23 | username: githubClientId, 24 | password: githubClientSecret 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /src/api/githubAPI.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import GhPolyglot from 'gh-polyglot'; 3 | import github from './base'; 4 | 5 | export const useGithubUserData = (username) => { 6 | const [userData, setUserData] = useState({}); 7 | const [loading, setLoading] = useState(false); 8 | const [error, setError] = useState({}); 9 | 10 | useEffect(() => { 11 | if (!username) return; 12 | 13 | const getUserData = async () => { 14 | try { 15 | setLoading(true); 16 | setError({ active: false, type: 200 }); 17 | 18 | const response = await github.get(`/users/${username}`); 19 | setUserData(response.data); 20 | setLoading(false); 21 | } catch (error) { 22 | console.log('Error', error); 23 | if (error.response) { 24 | if (error.response.status === 404) { 25 | setError({ active: true, type: 404 }); 26 | } else { 27 | setError({ active: true, type: error.response.status }); 28 | } 29 | } else { 30 | setError({ active: true, type: error }); 31 | console.log(error); 32 | } 33 | setLoading(false); 34 | } 35 | }; 36 | getUserData(); 37 | }, [username]); 38 | 39 | return [userData, loading, error]; 40 | }; 41 | 42 | // Using GhPolyglot library to get all the languages used 43 | export const useLangData = (username) => { 44 | const [langData, setLangData] = useState([]); 45 | const [loading, setLoading] = useState(false); 46 | const [error, setError] = useState({}); 47 | 48 | useEffect(() => { 49 | const getLangData = () => { 50 | setLoading(true); 51 | setError({ active: false, type: 200 }); 52 | 53 | const currentUser = new GhPolyglot(`${username}`); 54 | currentUser.userStats((err, stats) => { 55 | if (err === 'Not Found') { 56 | setError({ active: true, type: 404 }); 57 | } else if (err) { 58 | setError({ active: true, type: err }); 59 | console.log('err', err); 60 | } 61 | 62 | if (stats) { 63 | setLangData(stats); 64 | } 65 | }); 66 | setLoading(false); 67 | }; 68 | getLangData(); 69 | }, [username]); 70 | 71 | return [langData, loading, error]; 72 | }; 73 | 74 | export const useUserRepos = (username) => { 75 | const [repoData, setRepoData] = useState([]); 76 | const [loading, setLoading] = useState(false); 77 | const [error, setError] = useState({}); 78 | 79 | useEffect(() => { 80 | const getUserRepos = async () => { 81 | setLoading(true); 82 | setError({ active: false, type: 200 }); 83 | try { 84 | const findTotalRepo = await github.get(`/users/${username}`); 85 | const totalRepo = findTotalRepo.data.public_repos; 86 | let totalRequest = 1; 87 | // Reset Repo data to [] after rerendering 88 | setRepoData([]); 89 | 90 | // To get more than 100 repo find number of requests needed to make 91 | if (totalRepo > 0) { 92 | totalRequest = Math.ceil(totalRepo / 100); 93 | } 94 | 95 | // Get 100 repo in each request and add them to the old array 96 | for (let i = 1; i < totalRequest + 1; i++) { 97 | let response = await github.get( 98 | `/users/${username}/repos?per_page=100&page=${i}&sort=created:dsc` 99 | ); 100 | setRepoData((oldArray) => [...oldArray, ...response.data]); 101 | } 102 | 103 | setLoading(false); 104 | } catch (error) { 105 | if (error.response) { 106 | if (error.response.status === 404) { 107 | setError({ active: true, type: 404 }); 108 | } else { 109 | setError({ active: true, type: error.response.status }); 110 | } 111 | } else { 112 | setError({ active: true, type: error }); 113 | console.log(error); 114 | } 115 | 116 | setLoading(false); 117 | } 118 | }; 119 | getUserRepos(); 120 | }, [username]); 121 | 122 | return [repoData, loading, error]; 123 | }; 124 | 125 | export const useActivityData = (username) => { 126 | const [activityData, setActivityData] = useState([]); 127 | const [loading, setLoading] = useState(false); 128 | const [error, setError] = useState({}); 129 | 130 | useEffect(() => { 131 | const getActivityData = async () => { 132 | setLoading(true); 133 | setError({ active: false, type: 200 }); 134 | try { 135 | const response = await github.get(`/users/${username}/events?per_page=30`); 136 | 137 | setActivityData(response.data); 138 | setLoading(false); 139 | } catch (error) { 140 | if (error.response) { 141 | if (error.response.status === 404) { 142 | setError({ active: true, type: 404 }); 143 | } else { 144 | setError({ active: true, type: error.response.status }); 145 | } 146 | } else { 147 | setError({ active: true, type: error }); 148 | console.log(error); 149 | } 150 | 151 | setLoading(false); 152 | } 153 | }; 154 | getActivityData(); 155 | }, [username]); 156 | 157 | return [activityData, loading, error]; 158 | }; 159 | 160 | // export const useRateLimit = (username) => { 161 | // const [rateLimit, setRateLimit] = useState(null); 162 | // useEffect(() => { 163 | // const getRateLimit = async () => { 164 | // const response = await github.get( 165 | // "https://api.github.com/rate_limit" 166 | // ); 167 | // setRateLimit(response.data.rate); 168 | // console.log(response.data.rate); 169 | // }; 170 | // getRateLimit(); 171 | // }, [username]); 172 | // }; 173 | -------------------------------------------------------------------------------- /src/assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khusharth/gitpedia/53f6ca8cf9296f7927f76be73ab1b1ea6437b190/src/assets/demo.gif -------------------------------------------------------------------------------- /src/assets/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khusharth/gitpedia/53f6ca8cf9296f7927f76be73ab1b1ea6437b190/src/assets/loader.gif -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khusharth/gitpedia/53f6ca8cf9296f7927f76be73ab1b1ea6437b190/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/Activities.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { 4 | GoTrashcan, 5 | GoRepoForked, 6 | GoRepoPull, 7 | GoComment, 8 | GoGitBranch, 9 | GoPlus, 10 | GoRepoPush, 11 | GoStar, 12 | GoBook, 13 | GoIssueClosed, 14 | GoIssueOpened 15 | } from 'react-icons/go'; 16 | 17 | const ActivitiesContainer = styled.div` 18 | margin: 3rem auto; 19 | width: 100rem; 20 | 21 | & ul li a { 22 | text-decoration: none; 23 | &:link, 24 | &:visited { 25 | color: #0098f0; 26 | } 27 | 28 | &:hover, 29 | &:active { 30 | text-decoration: underline; 31 | } 32 | } 33 | 34 | @media only screen and (max-width: 1000px) { 35 | width: 100%; 36 | } 37 | `; 38 | 39 | const ActivitiesItem = styled.div` 40 | padding: 2rem 4rem; 41 | margin-bottom: 2rem; 42 | width: 100%; 43 | display: flex; 44 | flex-wrap: wrap; 45 | border-radius: 5px; 46 | box-shadow: 0 1rem 2rem -0.6rem rgba(0, 0, 0, 0.2); 47 | justify-content: space-between; 48 | background-color: ${(p) => p.theme.cardColor}; 49 | `; 50 | 51 | const ActivityDiv = styled.div` 52 | width: 75%; 53 | 54 | & svg { 55 | vertical-align: middle; 56 | margin-bottom: 4px; 57 | margin-right: 5px; 58 | } 59 | 60 | @media only screen and (max-width: 600px) { 61 | width: 100%; 62 | } 63 | `; 64 | 65 | const TimeDiv = styled.div` 66 | width: 25%; 67 | display: flex; 68 | justify-content: flex-end; 69 | @media only screen and (max-width: 600px) { 70 | justify-content: flex-start; 71 | margin-top: 1rem; 72 | width: 100%; 73 | } 74 | `; 75 | 76 | const FlexContainer = styled.div` 77 | width: 100%; 78 | display: flex; 79 | justify-content: center; 80 | `; 81 | 82 | const Activities = ({ activityData }) => { 83 | const extractActivity = () => { 84 | const message = activityData.map((activity) => { 85 | let icon = ''; 86 | let action = ''; 87 | let actionPerformed; // For Pull req 88 | let repoName = activity.repo.name; 89 | let time = new Date(activity.created_at).toDateString().split(' ').slice(1).join(' '); 90 | 91 | switch (activity.type) { 92 | case 'CommitCommentEvent': 93 | break; 94 | 95 | case 'CreateEvent': 96 | if (activity.payload.ref_type === 'branch') { 97 | icon = ; 98 | action = `Created a branch ${activity.payload.ref} in `; 99 | } else { 100 | icon = ; 101 | action = `Created a ${activity.payload.ref_type} in `; 102 | } 103 | break; 104 | 105 | case 'DeleteEvent': 106 | icon = ; 107 | action = `Deleted a ${activity.payload.ref_type} ${activity.payload.ref} from `; 108 | break; 109 | 110 | case 'ForkEvent': 111 | icon = ; 112 | action = `Forked a repository ${repoName} to `; 113 | repoName = activity.payload.forkee.full_name; 114 | break; 115 | 116 | case 'IssueCommentEvent': 117 | icon = ; 118 | actionPerformed = 119 | activity.payload.action.charAt(0).toUpperCase() + activity.payload.action.slice(1); 120 | 121 | action = `${actionPerformed} a comment on an issue in `; 122 | break; 123 | 124 | case 'IssuesEvent': 125 | if (activity.payload.action === 'closed') { 126 | icon = ; 127 | } else { 128 | icon = ; 129 | } 130 | actionPerformed = 131 | activity.payload.action.charAt(0).toUpperCase() + activity.payload.action.slice(1); 132 | 133 | action = `${actionPerformed} an issue in `; 134 | break; 135 | 136 | case 'PullRequestEvent': 137 | if (activity.payload.action === 'closed') { 138 | icon = ; 139 | } else { 140 | icon = ; 141 | } 142 | 143 | actionPerformed = 144 | activity.payload.action.charAt(0).toUpperCase() + activity.payload.action.slice(1); 145 | 146 | action = `${actionPerformed} a pull request in `; 147 | break; 148 | 149 | case 'PullRequestReviewCommentEvent': 150 | icon = ; 151 | actionPerformed = 152 | activity.payload.action.charAt(0).toUpperCase() + activity.payload.action.slice(1); 153 | 154 | action = `${actionPerformed} a comment on their pull request in `; 155 | break; 156 | 157 | case 'PushEvent': { 158 | icon = ; 159 | let commit = 'commit'; 160 | let branch = activity.payload.ref.slice(11); 161 | 162 | if (activity.payload.size > 1) { 163 | commit = 'commits'; 164 | } 165 | 166 | action = `Pushed ${activity.payload.size} ${commit} to ${branch} in `; 167 | break; 168 | } 169 | case 'WatchEvent': 170 | icon = ; 171 | action = 'Starred the repository '; 172 | break; 173 | 174 | case 'ReleaseEvent': 175 | icon = ; 176 | actionPerformed = 177 | activity.payload.action.charAt(0).toUpperCase() + activity.payload.action.slice(1); 178 | 179 | action = `${actionPerformed} a release in `; 180 | break; 181 | 182 | default: 183 | action = ''; 184 | } 185 | return { icon, action, repoName, time }; 186 | }); 187 | 188 | return message; 189 | }; 190 | 191 | const buildActivityList = () => { 192 | const messages = extractActivity(); 193 | 194 | if (messages.length !== 0) { 195 | return messages.map((message) => ( 196 |
  • 197 | 198 | 199 | 200 | {message.icon} {message.action} 201 | 202 | {message.repoName} 203 | 204 | {message.time} 205 | 206 |
  • 207 | )); 208 | } else { 209 | return ( 210 |
  • 211 | 212 | No recent activities found :( 213 | 214 |
  • 215 | ); 216 | } 217 | }; 218 | 219 | return ( 220 | <> 221 | 222 |
      {buildActivityList()}
    223 |
    224 | 225 | ); 226 | }; 227 | 228 | export default Activities; 229 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 4 | 5 | import Home from '../pages/Home'; 6 | import UserProfile from '../pages/UserProfile'; 7 | import { GlobalStyle } from '../style'; 8 | import ThemeProviderWrapper from 'src/contexts/ThemeProvider'; 9 | 10 | const App = () => { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /src/components/Button.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Button = styled.button` 4 | color: rgb(255, 255, 255); 5 | border: none; 6 | background-image: linear-gradient(to right, #0098f0, #00e1b5); 7 | padding: 1rem 1.5rem; 8 | border-radius: 5px; 9 | border-bottom: 2px solid transparent; 10 | cursor: pointer; 11 | align-items: center; 12 | transition: all 0.3s; 13 | 14 | &:hover { 15 | transform: scale(1.1); 16 | box-shadow: 0 1rem 2rem 0 rgba(0, 0, 0, 0.2); 17 | } 18 | 19 | &:focus { 20 | outline: 0; 21 | box-shadow: 0 1rem 2rem 0 rgba(0, 0, 0, 0.2); 22 | } 23 | 24 | &:active { 25 | transform: scale(1); 26 | } 27 | `; 28 | 29 | export default Button; 30 | -------------------------------------------------------------------------------- /src/components/Chart.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Bar, Doughnut, Pie } from 'react-chartjs-2'; 3 | import { ThemeContext } from 'styled-components'; 4 | import LanguageContext from '../contexts/LanguageContext'; 5 | 6 | // Different Colors used in Charts 7 | const bgColor = [ 8 | 'rgba(123, 13, 255, 0.7)', 9 | 'rgba(171, 36, 247, 0.7)', 10 | 'rgba(155, 110, 243, 0.4)', 11 | 'rgba(198, 128, 250, 0.6)', 12 | 'rgba(117, 221, 221, 0.8)', 13 | 'rgba(36, 196, 207, 0.8)', 14 | 'rgba(0, 146, 203, 0.8)', 15 | 'rgba(104, 106, 253, 1)', 16 | 'rgba(155, 110, 243, 1)', 17 | 'rgba(209,188,249,1)' 18 | ]; 19 | 20 | export const DoughnutChart = () => { 21 | const themeContext = useContext(ThemeContext); 22 | 23 | const data = { 24 | labels: [], 25 | datasets: [ 26 | { 27 | label: '', 28 | data: [], 29 | backgroundColor: bgColor, 30 | borderWidth: 0 31 | } 32 | ] 33 | }; 34 | 35 | return ( 36 | 37 | {(context) => { 38 | const LIMIT = 10; 39 | let labels = context.map((obj) => obj.label); 40 | 41 | // If more than LIMIT languages then reduce it to the limit 42 | if (labels.length >= LIMIT) { 43 | labels = labels.slice(0, LIMIT); 44 | } 45 | const value = context.map((obj) => obj.value).slice(0, LIMIT); 46 | data.labels = labels; 47 | data.datasets[0].data = value; 48 | 49 | return ( 50 | 67 | ); 68 | }} 69 | 70 | ); 71 | }; 72 | 73 | export const PieChart = ({ starData }) => { 74 | const themeContext = useContext(ThemeContext); 75 | 76 | let data = {}; 77 | 78 | if (starData.data) { 79 | // Only display chart if there is at least 1 star available 80 | let sum = starData.data.reduce((a, b) => a + b, 0); 81 | if (sum > 0) { 82 | data = { 83 | labels: starData.label, 84 | datasets: [ 85 | { 86 | label: '', 87 | data: starData.data, 88 | backgroundColor: bgColor, 89 | borderWidth: 0 90 | } 91 | ] 92 | }; 93 | } 94 | } 95 | 96 | return ( 97 | <> 98 | 117 | 118 | ); 119 | }; 120 | 121 | export const BarChart = ({ sizeData }) => { 122 | const themeContext = useContext(ThemeContext); 123 | 124 | const data = { 125 | labels: [], 126 | datasets: [ 127 | { 128 | label: '', 129 | data: [], 130 | backgroundColor: bgColor 131 | } 132 | ] 133 | }; 134 | data.labels = sizeData.label; 135 | data.datasets[0].data = sizeData.data; 136 | 137 | const scales = { 138 | xAxes: [ 139 | { 140 | ticks: { 141 | fontColor: themeContext.textColor, 142 | fontFamily: "'Roboto', sans-serif", 143 | fontSize: 12 144 | } 145 | } 146 | ], 147 | yAxes: [ 148 | { 149 | ticks: { 150 | fontColor: themeContext.textColor, 151 | beginAtZero: true, 152 | fontFamily: "'Roboto', sans-serif", 153 | fontSize: 12 154 | } 155 | } 156 | ] 157 | }; 158 | 159 | return ( 160 | <> 161 | 178 | 179 | ); 180 | }; 181 | -------------------------------------------------------------------------------- /src/components/Error.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { GoAlert } from 'react-icons/go'; 4 | 5 | const ErrorContainer = styled.div` 6 | height: calc(100vh - 130px); 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | `; 11 | 12 | const ErrorDiv = styled.div` 13 | padding: 2rem 4rem; 14 | border-radius: 5px; 15 | background-color: ${(p) => p.theme.cardColor}; 16 | box-shadow: 0 1rem 2rem 0 rgba(0, 0, 0, 0.2); 17 | 18 | & svg { 19 | vertical-align: middle; 20 | margin-bottom: 4px; 21 | margin-right: 1rem; 22 | } 23 | `; 24 | 25 | const Error = ({ error }) => { 26 | return ( 27 | <> 28 | 29 | 30 | 31 | 32 | 33 | {error.type === 404 34 | ? 'No user found! Please try again :)' 35 | : 'Oops! Some error occured. Please try again :)'} 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default Error; 43 | -------------------------------------------------------------------------------- /src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { GoStar } from 'react-icons/go'; 4 | 5 | const FooterContainer = styled.footer` 6 | display: flex; 7 | text-align: center; 8 | flex-direction: column; 9 | align-items: center; 10 | height: 6rem; 11 | padding: 0 2rem 1rem 2rem; 12 | font-size: 1.4rem; 13 | 14 | & svg { 15 | vertical-align: middle; 16 | } 17 | 18 | @media only screen and (max-width: 600px) { 19 | height: 8rem; 20 | } 21 | `; 22 | 23 | const ProjectLink = styled.a` 24 | text-decoration: none; 25 | 26 | &:link, 27 | &:visited { 28 | color: #0098f0; 29 | } 30 | 31 | &:hover, 32 | &:active { 33 | text-decoration: underline; 34 | } 35 | `; 36 | 37 | const Footer = () => { 38 | return ( 39 | 40 |
    41 | If you like this project then you can show some love by giving it a :) 42 |
    43 |
    44 | 48 | khusharth/gitpedia 49 | 50 |
    51 |
    52 | ); 53 | }; 54 | 55 | export default Footer; 56 | -------------------------------------------------------------------------------- /src/components/Form.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import styled from 'styled-components'; 4 | import Button from './Button'; 5 | import { FaSearch } from 'react-icons/fa'; 6 | 7 | const Form = styled.form` 8 | display: flex; 9 | flex-wrap: wrap; 10 | justify-content: center; 11 | align-items: center; 12 | 13 | & button { 14 | padding: 1.2rem 1.5rem; 15 | } 16 | 17 | @media only screen and (max-width: 600px) { 18 | & button { 19 | align-self: flex-start; 20 | } 21 | } 22 | 23 | @media only screen and (max-width: 400px) { 24 | & button { 25 | height: auto; 26 | } 27 | } 28 | 29 | & span { 30 | } 31 | `; 32 | 33 | const Input = styled.input` 34 | font-size: 2.2rem; 35 | font-family: inherit; 36 | color: inherit; 37 | padding: 1.2rem 1.6rem; 38 | border-radius: 5px; 39 | border: none; 40 | background-color: ${(p) => p.theme.inputColor}; 41 | border-bottom: 3px solid transparent; 42 | margin-left: 10px; 43 | margin-right: 15px; 44 | transition: all 0.3s; 45 | 46 | &:focus { 47 | outline: none; 48 | box-shadow: 0 1rem 2rem 0 rgba(0, 0, 0, 0.2); 49 | border-bottom: 3px solid #0098f0; 50 | } 51 | 52 | &::placeholder { 53 | color: #aaa; 54 | } 55 | 56 | @media only screen and (max-width: 600px) { 57 | text-align: center; 58 | margin: 0; 59 | margin-right: 15px; 60 | margin-bottom: 10px; 61 | } 62 | `; 63 | 64 | const Span = styled.span` 65 | font-size: 2.4rem; 66 | display: ${(p) => (p.displaySpan ? 'inline-block' : 'none')}; 67 | 68 | @media only screen and (max-width: 600px) { 69 | display: none; 70 | } 71 | `; 72 | 73 | const SearchForm = ({ displaySpan }) => { 74 | const [user, updateUser] = useState(''); 75 | const history = useHistory(); 76 | 77 | const onFormSubmit = async (event, user) => { 78 | event.preventDefault(); 79 | 80 | // If value of user is not blank then only go to next page 81 | if (user) { 82 | history.push(`/user/${user}`); 83 | } 84 | }; 85 | 86 | return ( 87 |
    onFormSubmit(e, user)} displaySpan={displaySpan}> 88 | ~ $ git --view 89 | updateUser(e.target.value)} 93 | type="text" 94 | placeholder="Enter Github Username" 95 | /> 96 | 99 |
    100 | ); 101 | }; 102 | 103 | export default SearchForm; 104 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import styled, { ThemeContext } from 'styled-components'; 3 | import { Link } from 'react-router-dom'; 4 | import Logo from './Logo'; 5 | import Toggle from './Toggle'; 6 | import Form from './Form'; 7 | 8 | const StyledHeader = styled.header` 9 | background-color: ${(p) => p.theme.cardColor}; 10 | min-height: 7rem; 11 | box-shadow: 0 1rem 2rem 0 rgba(0, 0, 0, 0.1); 12 | display: flex; 13 | flex-wrap: wrap; 14 | align-items: center; 15 | padding: 0.5rem 6rem; 16 | justify-content: space-between; 17 | 18 | @media only screen and (max-width: 767px) { 19 | padding: 0.5rem 2rem; 20 | } 21 | 22 | & input { 23 | font-size: 2rem; 24 | } 25 | 26 | & svg { 27 | vertical-align: middle; 28 | font-size: 2rem; 29 | } 30 | 31 | @media only screen and (max-width: 600px) { 32 | & input { 33 | margin-bottom: 5px; 34 | padding: 1rem; 35 | width: 75%; 36 | } 37 | 38 | & svg { 39 | vertical-align: unset; 40 | } 41 | } 42 | 43 | & a { 44 | margin-top: 0.5rem; 45 | } 46 | 47 | & a:focus { 48 | outline: none; 49 | } 50 | 51 | @media only screen and (max-width: 633px) { 52 | & form { 53 | order: 1; 54 | margin-top: 0.7rem; 55 | margin-left: auto; 56 | margin-right: auto; 57 | } 58 | } 59 | `; 60 | 61 | const Header = () => { 62 | const { id, setTheme } = useContext(ThemeContext); 63 | 64 | return ( 65 | 66 | 67 | 68 | 69 |
    70 | 71 | 72 | ); 73 | }; 74 | 75 | export default Header; 76 | -------------------------------------------------------------------------------- /src/components/Loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import loader from '../assets/loader.gif'; 4 | 5 | const LoaderContainer = styled.div` 6 | height: calc(100vh - 150px); 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | `; 11 | 12 | const Loader = () => { 13 | return ( 14 | 15 | Loader 16 | 17 | ); 18 | }; 19 | 20 | export default Loader; 21 | -------------------------------------------------------------------------------- /src/components/Logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from '../assets/logo.png'; 3 | 4 | const Logo = (props) => { 5 | return ( 6 | <> 7 | logo 8 | 9 | ); 10 | }; 11 | 12 | export default Logo; 13 | -------------------------------------------------------------------------------- /src/components/MaterialTabs.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { withStyles } from '@material-ui/styles'; 4 | import Tab from '@material-ui/core/Tab'; 5 | import Tabs from '@material-ui/core/Tabs'; 6 | 7 | const TabsContainer = styled.div` 8 | background-color: ${(p) => p.theme.cardColor}; 9 | box-shadow: 10 | 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 11 | 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 12 | 0px 1px 3px 0px rgba(0, 0, 0, 0.12); 13 | border-radius: 4px; 14 | `; 15 | // Object for configuring default material UI styles 16 | const styles = { 17 | indicator: { 18 | backgroundColor: '#1890ff' 19 | }, 20 | centered: { 21 | justifyContent: 'space-around' 22 | }, 23 | tab: { 24 | fontFamily: "'Roboto', sans-serif", 25 | fontSize: '1.5rem' 26 | }, 27 | tabRoot: { 28 | color: '#999', 29 | '&:hover': { 30 | // color: "#ffffff", 31 | // opacity: 1, 32 | }, 33 | '&$tabSelected': { 34 | color: '#1890ff' 35 | }, 36 | textTransform: 'initial' 37 | }, 38 | tabSelected: { 39 | color: '#1890ff' 40 | } 41 | }; 42 | 43 | const MaterialTabs = (props) => { 44 | const [selectedTab, setSelectedTab] = useState(0); 45 | 46 | const handleChange = (event, newValue) => { 47 | setSelectedTab(newValue); 48 | }; 49 | 50 | const tabStyle = { 51 | root: props.classes.tabRoot, 52 | selected: props.classes.tabSelected 53 | }; 54 | 55 | const { indicator, centered, tab } = props.classes; 56 | return ( 57 | <> 58 | 59 | 68 | 69 | 70 | 71 | 72 | 73 | {selectedTab === 0 && props.tab1} 74 | {selectedTab === 1 && props.tab2} 75 | {selectedTab === 2 && props.tab3} 76 | 77 | ); 78 | }; 79 | 80 | export default withStyles(styles)(MaterialTabs); 81 | -------------------------------------------------------------------------------- /src/components/Profile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { 4 | GoLocation, 5 | GoGlobe, 6 | GoMarkGithub, 7 | GoBriefcase, 8 | GoMail, 9 | GoCalendar, 10 | GoPackage, 11 | GoCode 12 | } from 'react-icons/go'; 13 | import LanguageContext from '../contexts/LanguageContext'; 14 | import Button from './Button'; 15 | 16 | const ProfileSection = styled.section` 17 | padding: 2rem 6rem; 18 | padding-bottom: 0rem; 19 | display: flex; 20 | flex-direction: column; 21 | 22 | @media only screen and (max-width: 900px) { 23 | padding: 1.5rem 2rem; 24 | } 25 | `; 26 | 27 | const UserContainer = styled.div` 28 | height: 100%; 29 | padding: 2rem 0; 30 | display: flex; 31 | flex-direction: column; 32 | align-items: center; 33 | 34 | @media only screen and (max-width: 600px) { 35 | padding-top: 10rem; 36 | } 37 | `; 38 | 39 | const UserInfoDiv = styled.div` 40 | display: flex; 41 | justify-content: center; 42 | background-color: ${(p) => p.theme.cardColor}; 43 | padding: 4rem; 44 | border-radius: 5px; 45 | box-shadow: 0 1rem 2rem 0 rgba(0, 0, 0, 0.2); 46 | 47 | & ul li { 48 | font-size: 1.8rem; 49 | padding: 0.3rem 0; 50 | } 51 | 52 | & ul li h1 { 53 | font-size: 3rem; 54 | font-weight: 400; 55 | } 56 | 57 | & ul li:last-of-type { 58 | padding-top: 0.8rem; 59 | } 60 | 61 | & ul li a { 62 | text-decoration: none; 63 | &:link, 64 | &:visited { 65 | color: #0098f0; 66 | } 67 | 68 | &:hover, 69 | &:active { 70 | text-decoration: underline; 71 | } 72 | } 73 | 74 | @media only screen and (max-width: 600px) { 75 | flex-direction: column; 76 | align-items: center; 77 | padding: 3rem 4rem; 78 | 79 | & ul li { 80 | text-align: center; 81 | } 82 | } 83 | 84 | @media only screen and (max-width: 500px) { 85 | width: 100%; 86 | padding: 3rem 2rem; 87 | } 88 | `; 89 | 90 | const ProfileImgDiv = styled.div` 91 | margin-right: 2.5rem; 92 | position: relative; 93 | height: 200px; 94 | width: 100px; 95 | @media only screen and (max-width: 600px) { 96 | margin-right: 0; 97 | margin-bottom: 2.5rem; 98 | width: 100%; 99 | height: 100px; 100 | } 101 | 102 | & img { 103 | position: absolute; 104 | top: 0; 105 | left: -10rem; 106 | vertical-align: middle; 107 | text-align: center; 108 | border-radius: 2px; 109 | 110 | @media only screen and (max-width: 600px) { 111 | top: -10rem; 112 | right: 0; 113 | left: 0; 114 | margin: 0 auto; 115 | } 116 | } 117 | `; 118 | 119 | const DetailList = styled.ul` 120 | margin-top: 3rem; 121 | border-radius: 5px; 122 | background-color: ${(p) => p.theme.cardColor}; 123 | padding: 2rem 4rem; 124 | box-shadow: 0 1rem 2rem 0 rgba(0, 0, 0, 0.2); 125 | 126 | @media only screen and (max-width: 600px) { 127 | padding: 3rem 4rem; 128 | } 129 | 130 | @media only screen and (max-width: 500px) { 131 | width: 100%; 132 | } 133 | 134 | & ul li { 135 | text-align: center; 136 | } 137 | 138 | & ul li span a button { 139 | font-weight: 500; 140 | } 141 | `; 142 | const FlexContainer = styled.div` 143 | display: flex; 144 | 145 | @media only screen and (max-width: 600px) { 146 | flex-direction: column; 147 | justify-content: center; 148 | } 149 | `; 150 | 151 | const LocationDiv = styled.div` 152 | @media only screen and (max-width: 600px) { 153 | padding-top: 0.6rem; 154 | } 155 | `; 156 | 157 | const IconSpan = styled.span` 158 | display: ${(p) => (p.available ? 'inline' : 'none')}; 159 | margin-right: ${(p) => (p.company ? '1.5rem' : '0')}; 160 | 161 | & svg { 162 | vertical-align: middle; 163 | margin-bottom: 4px; 164 | } 165 | 166 | & a button svg { 167 | margin-bottom: 0; 168 | } 169 | 170 | @media only screen and (max-width: 600px) { 171 | margin-right: 0; 172 | } 173 | `; 174 | 175 | const Span = styled.span` 176 | & svg { 177 | vertical-align: middle; 178 | margin-bottom: 3px; 179 | } 180 | 181 | margin-right: 0.5rem; 182 | `; 183 | 184 | const Profile = (props) => { 185 | const { 186 | avatar_url, 187 | email, 188 | html_url, 189 | login, 190 | company, 191 | location, 192 | blog, 193 | name, 194 | public_repos, 195 | created_at 196 | } = props.userData; 197 | 198 | let website = blog; 199 | 200 | if (blog && blog.slice(0, 4) !== 'http') { 201 | website = `http://${blog}`; 202 | } 203 | 204 | const date = new Date(created_at); 205 | const joinedDate = date.toDateString().split(' ').slice(1).join(' '); 206 | 207 | return ( 208 | 209 | 210 | 211 | 212 | avatar 213 | 214 |
      215 |
    • 216 |

      {name}

      217 |
    • 218 |
    • 219 | 220 |
      221 | {/* If available = true then only show the component */} 222 | 223 | {company} 224 | 225 |
      226 | 227 | 228 | {location} 229 | 230 | 231 |
      232 |
    • 233 |
    • 234 | 235 | {email} 236 | 237 |
    • 238 |
    • 239 | 240 | {' '} 241 | 242 | {blog} 243 | 244 | 245 |
    • 246 |
    • 247 | 248 | 253 | 256 | 257 | 258 |
    • 259 |
    260 |
    261 | 262 |
      263 |
    • 264 | {' '} 265 | 266 | 267 | {' '} 268 | Joined github on {joinedDate} 269 |
    • 270 |
    • 271 | {' '} 272 | 273 | 274 | {' '} 275 | Since have created {public_repos} projects 276 |
    • 277 | 278 | {(context) => { 279 | const langCount = context.length; 280 | return ( 281 |
    • 282 | 283 | 284 | {' '} 285 | Using {langCount} different languages 286 |
    • 287 | ); 288 | }} 289 |
      290 |
    291 |
    292 |
    293 |
    294 | ); 295 | }; 296 | 297 | export default Profile; 298 | -------------------------------------------------------------------------------- /src/components/Stats/Stats.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import styled from 'styled-components'; 3 | import { GoRepo, GoOrganization, GoPerson, GoStar } from 'react-icons/go'; 4 | import { DoughnutChart, PieChart, BarChart } from '../Chart'; 5 | 6 | import { calculateTotalStars, calculateMostStarredRepos, calculateMaxSizeRepos } from './utils'; 7 | 8 | const StatsContainer = styled.div` 9 | margin-top: 3rem; 10 | margin-bottom: 2rem; 11 | display: flex; 12 | justify-content: space-between; 13 | flex-wrap: wrap; 14 | 15 | @media only screen and (max-width: 600px) { 16 | justify-content: space-around; 17 | } 18 | `; 19 | 20 | const StatsDiv = styled.div` 21 | display: inline-block; 22 | background-color: ${(p) => { 23 | if (p.secondary) return '#00b7e1'; 24 | else if (p.tertiary) return '#00cbbe'; 25 | else if (p.quad) return '#00d0a7'; 26 | else return '#0098f0'; 27 | }}; 28 | color: rgb(255, 255, 255); 29 | margin-top: 0; 30 | margin-right: 1rem; 31 | box-shadow: 0 1rem 2rem 0 rgba(0, 0, 0, 0.2); 32 | padding: 1.5rem 3rem; 33 | min-width: 25rem; 34 | border-radius: 5px; 35 | font-size: 2.2rem; 36 | 37 | & svg { 38 | vertical-align: middle; 39 | } 40 | 41 | @media only screen and (max-width: 1174px) { 42 | margin-top: ${(p) => (p.quad ? '2rem' : '0rem')}; 43 | } 44 | 45 | @media only screen and (max-width: 749px) { 46 | margin-top: ${(p) => (p.tertiary || p.quad ? '2rem' : '0rem')}; 47 | } 48 | 49 | @media only screen and (max-width: 600px) { 50 | text-align: center; 51 | } 52 | 53 | @media only screen and (max-width: 516px) { 54 | margin-top: ${(p) => (p.primary ? '0rem' : '2rem')}; 55 | } 56 | 57 | @media only screen and (max-width: 498px) { 58 | margin-right: 0; 59 | } 60 | `; 61 | 62 | const RoundChartContainer = styled.div` 63 | display: flex; 64 | justify-content: space-between; 65 | flex-wrap: wrap; 66 | padding: 3rem 0; 67 | 68 | @media only screen and (max-width: 1290px) { 69 | justify-content: space-around; 70 | } 71 | 72 | @media only screen and (max-width: 770px) { 73 | padding-bottom: 0; 74 | } 75 | `; 76 | 77 | const ChartDiv = styled.div` 78 | display: inline-block; 79 | margin-right: 1rem; 80 | padding: 1.5rem 6rem; 81 | padding: ${(p) => (p.chart1 ? '1.5rem 3rem' : '1.5rem 5rem')}; 82 | max-height: 55rem; 83 | border-radius: 5px; 84 | min-width: 38rem; 85 | box-shadow: 0 1rem 2rem 0 rgba(0, 0, 0, 0.2); 86 | background-color: ${(p) => p.theme.cardColor}; 87 | 88 | & canvas { 89 | max-height: 40rem; 90 | } 91 | 92 | @media only screen and (max-width: 1290px) { 93 | margin-top: ${(p) => (p.chart3 ? '5rem' : '0rem')}; 94 | } 95 | 96 | @media only screen and (max-width: 1140px) { 97 | width: ${(p) => (p.tertiary ? '100%' : '0rem')}; 98 | } 99 | 100 | @media only screen and (max-width: 770px) { 101 | min-width: 80%; 102 | margin-top: ${(p) => (p.chart3 ? '0rem' : '0rem')}; 103 | margin-bottom: ${(p) => (p.chart5 ? '0rem' : '5rem')}; 104 | } 105 | 106 | @media only screen and (max-width: 600px) { 107 | min-width: 100%; 108 | } 109 | `; 110 | 111 | const ChartHeading = styled.h1` 112 | font-weight: 400; 113 | font-size: 2.6rem; 114 | margin-bottom: 2rem; 115 | text-align: center; 116 | `; 117 | 118 | const IconSpan = styled.span` 119 | margin-left: 0.8rem; 120 | font-size: 1.7rem; 121 | font-weight: 500; 122 | `; 123 | 124 | const Stats = ({ userData, repoData }) => { 125 | const [starData, setStarData] = useState({}); 126 | const [sizeData, setSizeData] = useState({}); 127 | const [totalStars, setTotalStars] = useState(null); 128 | 129 | useEffect(() => { 130 | const getMostStarredRepos = () => { 131 | setStarData(calculateMostStarredRepos(repoData)); 132 | }; 133 | 134 | const getTotalStars = () => { 135 | setTotalStars(calculateTotalStars(repoData)); 136 | }; 137 | 138 | const getMaxSizeRepos = () => { 139 | setSizeData(calculateMaxSizeRepos(repoData)); 140 | }; 141 | 142 | if (repoData.length) { 143 | getMostStarredRepos(); 144 | getMaxSizeRepos(); 145 | getTotalStars(); 146 | } 147 | }, [repoData]); 148 | 149 | return ( 150 | <> 151 | 152 | 153 |
    154 | 155 | Repositories 156 |
    157 |

    {userData.public_repos}

    158 |
    159 | 160 |
    161 | 162 | Total Stars 163 |
    164 |

    {totalStars}

    165 |
    166 | 167 |
    168 | 169 | Followers 170 |
    171 |

    {userData.followers}

    172 |
    173 | 174 |
    175 | 176 | Following 177 |
    178 |

    {userData.following}

    179 |
    180 |
    181 | 182 | 183 | 184 | Largest in Size(kb) 185 | 186 | 187 | 188 | Top Languages 189 | 190 | 191 | 192 | Most Starred 193 | 194 | 195 | 196 | 197 | ); 198 | }; 199 | 200 | export default Stats; 201 | -------------------------------------------------------------------------------- /src/components/Stats/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { calculateTotalStars } from '../utils'; 2 | import { RepoData } from '../../../types'; 3 | import { mockRepoData } from '../../../mock.data'; 4 | 5 | describe('calculateTotalStars', () => { 6 | test('should return the correct total stars of non-forked repositories', () => { 7 | // Sample data for the repoData array with both forked and non-forked repositories 8 | const repoData = [ 9 | { ...mockRepoData, name: 'repo1', fork: false, stargazers_count: 10 }, 10 | { ...mockRepoData, name: 'repo2', fork: true, stargazers_count: 5 }, 11 | { ...mockRepoData, name: 'repo3', fork: false, stargazers_count: 15 } 12 | ]; 13 | 14 | // Call getTotalStars with the sample repoData 15 | const totalStars = calculateTotalStars(repoData); 16 | 17 | // Verify that the totalStars is calculated correctly 18 | expect(totalStars).toBe(25); 19 | }); 20 | 21 | test('should return 0 when all repos are forked repositories', () => { 22 | const repoData = [ 23 | { ...mockRepoData, name: 'repo1', fork: true, stargazers_count: 10 }, 24 | { ...mockRepoData, name: 'repo2', fork: true, stargazers_count: 5 }, 25 | { ...mockRepoData, name: 'repo3', fork: true, stargazers_count: 15 } 26 | ]; 27 | 28 | // Call getTotalStars with the empty repoData 29 | const totalStars = calculateTotalStars(repoData); 30 | 31 | // Verify that the totalStars is 0 32 | expect(totalStars).toBe(0); 33 | }); 34 | 35 | test('should return 0 when there are no repos', () => { 36 | // Sample data for an empty repoData array 37 | const repoData: RepoData = []; 38 | 39 | // Call getTotalStars with the empty repoData 40 | const totalStars = calculateTotalStars(repoData); 41 | 42 | // Verify that the totalStars is 0 43 | expect(totalStars).toBe(0); 44 | }); 45 | 46 | it('should handle missing stargazers_count property', () => { 47 | // Sample data with missing stargazers_count property in one of the repos 48 | const repoData = [ 49 | { ...mockRepoData, name: 'repo1', fork: false, stargazers_count: 10 }, 50 | { ...mockRepoData, name: 'repo2', fork: true, stargazers_count: 5 }, 51 | { ...mockRepoData, name: 'repo3', fork: false, stargazers_count: undefined } 52 | ]; 53 | 54 | // Call getTotalStars with the sample repoData 55 | const totalStars = calculateTotalStars(repoData); 56 | 57 | // Verify that the missing stargazers_count property is handled gracefully (ignored in the calculation) 58 | expect(totalStars).toBe(10); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/components/Stats/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Stats'; 2 | -------------------------------------------------------------------------------- /src/components/Stats/utils.ts: -------------------------------------------------------------------------------- 1 | import { RepoData } from '../../types'; 2 | 3 | const MAX_REPO_TO_SHOW_ON_GRAPH = 5; 4 | const sortProperties = { 5 | STARGAZERS_COUNT: 'stargazers_count', 6 | SIZE: 'size' 7 | } as const; 8 | 9 | /** 10 | * returns total stars a user has from all their repos (excluding forks) 11 | */ 12 | const calculateTotalStars = (repoData: RepoData) => { 13 | const myRepos = repoData.filter((repo) => !repo.fork).map((repo) => repo.stargazers_count ?? 0); 14 | const totalStars = myRepos.reduce((a, b) => a + b, 0); 15 | 16 | return totalStars; 17 | }; 18 | 19 | /** 20 | * gets stats (no. of stars) for the top 5 most starred repos of the user 21 | */ 22 | const calculateMostStarredRepos = (repoData: RepoData) => { 23 | const sortProperty = sortProperties.STARGAZERS_COUNT; 24 | 25 | const mostStarredRepos = repoData 26 | .filter((repo) => !repo.fork) 27 | .sort((a, b) => (b[sortProperty] ?? 0) - (a[sortProperty] ?? 0)) 28 | .slice(0, MAX_REPO_TO_SHOW_ON_GRAPH); 29 | 30 | // Label and data needed for displaying Charts 31 | const label = mostStarredRepos.map((repo) => repo.name); 32 | const data = mostStarredRepos.map((repo) => repo[sortProperty]); 33 | 34 | return { label, data }; 35 | }; 36 | 37 | /** 38 | * gets size for the top 5 largest repos by size of the user 39 | */ 40 | const calculateMaxSizeRepos = (repoData: RepoData) => { 41 | const sortProperty = sortProperties.SIZE; 42 | 43 | const mostStarredRepos = repoData 44 | .filter((repo) => !repo.fork) 45 | .sort((a, b) => b[sortProperty] - a[sortProperty]) 46 | .slice(0, MAX_REPO_TO_SHOW_ON_GRAPH); 47 | 48 | const label = mostStarredRepos.map((repo) => repo.name); 49 | const data = mostStarredRepos.map((repo) => repo[sortProperty]); 50 | 51 | return { label, data }; 52 | }; 53 | 54 | export { calculateTotalStars, calculateMostStarredRepos, calculateMaxSizeRepos }; 55 | -------------------------------------------------------------------------------- /src/components/Timeline.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import TimelineItem from './TimelineItem'; 4 | 5 | const TimelineContainer = styled.div` 6 | position: relative; 7 | margin: 3rem auto; 8 | width: 100rem; 9 | 10 | &:before { 11 | content: ''; 12 | position: absolute; 13 | left: 50%; 14 | width: 2px; 15 | height: 100%; 16 | background: #c5c5c5; 17 | 18 | @media only screen and (max-width: 767px) { 19 | left: 20px; 20 | } 21 | } 22 | 23 | & ul { 24 | padding: 0; 25 | margin: 0; 26 | } 27 | 28 | & ul li { 29 | position: relative; 30 | line-height: normal; 31 | width: 50%; 32 | padding: 2rem 4rem 4rem 4rem; 33 | box-sizing: border-box; 34 | 35 | @media only screen and (max-width: 767px) { 36 | margin-bottom: 3rem; 37 | } 38 | 39 | @media only screen and (max-width: 600px) { 40 | padding: 2rem 0rem 4rem 4rem; 41 | } 42 | } 43 | 44 | & ul li a { 45 | text-decoration: none; 46 | color: inherit; 47 | } 48 | 49 | & ul li h1 svg { 50 | vertical-align: middle; 51 | } 52 | 53 | & ul li:nth-child(odd) { 54 | float: left; 55 | text-align: right; 56 | clear: both; 57 | 58 | @media only screen and (max-width: 767px) { 59 | width: 100%; 60 | text-align: left; 61 | padding-left: 5rem; 62 | padding-bottom: 5rem; 63 | } 64 | } 65 | 66 | & ul li:nth-child(even) { 67 | float: right; 68 | text-align: left; 69 | clear: both; 70 | 71 | @media only screen and (max-width: 767px) { 72 | width: 100%; 73 | text-align: left; 74 | padding-left: 5rem; 75 | padding-bottom: 5rem; 76 | } 77 | } 78 | 79 | & ul li:nth-child(odd):before { 80 | content: ''; 81 | position: absolute; 82 | right: -7px; 83 | top: 25px; 84 | width: 13px; 85 | height: 13px; 86 | background-image: linear-gradient(to right, #0098f0, #00f2c3); 87 | border-radius: 50%; 88 | box-shadow: 0 0 0 4px rgba(0, 242, 195, 0.2); 89 | 90 | @media only screen and (max-width: 767px) { 91 | top: -18px; 92 | left: 14px; 93 | } 94 | } 95 | 96 | & ul li:nth-child(even):before { 97 | content: ''; 98 | position: absolute; 99 | left: -5px; 100 | top: 25px; 101 | width: 13px; 102 | height: 13px; 103 | background-image: linear-gradient(to right, #0098f0, #00f2c3); 104 | border-radius: 50%; 105 | box-shadow: 0 0 0 4px rgba(0, 242, 195, 0.2); 106 | 107 | @media only screen and (max-width: 767px) { 108 | top: -18px; 109 | left: 14px; 110 | } 111 | } 112 | 113 | & ul li:nth-child(odd) .time { 114 | position: absolute; 115 | top: 12px; 116 | right: -165px; 117 | 118 | @media only screen and (max-width: 767px) { 119 | top: -30px; 120 | left: 50px; 121 | right: inherit; 122 | } 123 | } 124 | 125 | & ul li:nth-child(even) .time { 126 | position: absolute; 127 | top: 12px; 128 | left: -165px; 129 | 130 | @media only screen and (max-width: 767px) { 131 | top: -30px; 132 | left: 50px; 133 | right: inherit; 134 | } 135 | } 136 | 137 | & ul li .time h4 { 138 | font-size: 14px; 139 | font-weight: 500; 140 | } 141 | 142 | @media only screen and (max-width: 1000px) { 143 | width: 100%; 144 | } 145 | 146 | @media only screen and (max-width: 767px) { 147 | width: 100%; 148 | margin-top: 7rem; 149 | padding-bottom: 0; 150 | } 151 | `; 152 | 153 | const TimeDiv = styled.div` 154 | background-image: linear-gradient(to right, #0098f0, #00f2c3); 155 | margin: 0; 156 | padding: 8px 16px; 157 | color: #fff; 158 | border-radius: 18px; 159 | box-shadow: 0 0 0 3px rgba(0, 242, 195, 0.2); 160 | `; 161 | 162 | const ClearFloat = styled.div` 163 | clear: both; 164 | `; 165 | 166 | const Timeline = ({ repoData }) => { 167 | const buildRepoTimeline = () => { 168 | return repoData.map((repo) => ( 169 |
  • 170 | 179 | 180 |

    {new Date(repo.updated_at).toDateString().split(' ').slice(1).join(' ')}

    181 |
    182 |
  • 183 | )); 184 | }; 185 | 186 | return ( 187 | <> 188 | 189 |
      190 | {buildRepoTimeline()} 191 | 192 |
    193 |
    194 | 195 | ); 196 | }; 197 | 198 | export default Timeline; 199 | -------------------------------------------------------------------------------- /src/components/TimelineItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { GoRepo, GoRepoForked, GoStar, GoPrimitiveDot } from 'react-icons/go'; 4 | 5 | const ItemContainer = styled.div` 6 | display: inline-block; 7 | background-color: ${(p) => p.theme.cardColor}; 8 | padding: 2rem; 9 | border-radius: 5px; 10 | box-shadow: 0 1rem 2rem 0 rgb(0, 0, 0, 0.2); 11 | min-width: 30rem; 12 | 13 | & h1 { 14 | padding-bottom: 1.5rem; 15 | font-weight: 500; 16 | } 17 | 18 | @media only screen and (max-width: 600px) { 19 | width: 100%; 20 | } 21 | `; 22 | 23 | const FooterSpan = styled.span` 24 | display: ${(p) => (p.available ? 'inline' : 'none')}; 25 | font-size: 1.5rem; 26 | margin-right: 1rem; 27 | `; 28 | 29 | const ItemFooter = styled.div` 30 | margin-top: 3rem; 31 | display: flex; 32 | justify-content: space-between; 33 | `; 34 | 35 | const TimelineItem = ({ title, description, language, forks, size, stars, url }) => { 36 | return ( 37 | <> 38 | 39 | 40 |

    41 | 42 | 43 | {' '} 44 | {title} 45 |

    46 |
    {description}
    47 | 48 |
    49 | 50 | {language} 51 | 52 | 53 | {stars} 54 | 55 | 56 | {forks} 57 | 58 |
    59 |
    {Number(size).toLocaleString()} Kb
    60 |
    61 |
    62 |
    63 | 64 | ); 65 | }; 66 | 67 | export default TimelineItem; 68 | -------------------------------------------------------------------------------- /src/components/Toggle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from './Button'; 3 | import styled from 'styled-components'; 4 | import { FaMoon, FaSun } from 'react-icons/fa'; 5 | 6 | const ToggleSpan = styled.span` 7 | padding-left: 12rem; 8 | 9 | @media only screen and (max-width: 1100px) { 10 | padding-left: 10rem; 11 | } 12 | 13 | @media only screen and (max-width: 950px) { 14 | padding-left: 9rem; 15 | } 16 | 17 | @media only screen and (max-width: 950px) { 18 | padding-left: 7rem; 19 | } 20 | 21 | @media only screen and (max-width: 780px) { 22 | padding-left: 3rem; 23 | } 24 | 25 | @media only screen and (max-width: 650px) { 26 | padding-left: 1rem; 27 | } 28 | 29 | & svg { 30 | vertical-align: middle; 31 | font-size: 2rem; 32 | } 33 | `; 34 | 35 | const Toggle = ({ isDark, onToggle }) => { 36 | return ( 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default Toggle; 44 | -------------------------------------------------------------------------------- /src/components/__tests__/Form.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router } from 'react-router-dom'; 3 | import { render, screen } from '@testing-library/react'; 4 | import user from '@testing-library/user-event'; 5 | import { Form } from '../'; 6 | 7 | test('renders a input with a search cta to enter github username', () => { 8 | const userName = 'khusharth'; 9 | 10 | render( 11 | 12 | 13 | 14 | ); 15 | 16 | const mockHistoryPushState = jest.spyOn(window.history, 'pushState'); 17 | 18 | const input = screen.getByLabelText(/enter github username/i); 19 | const searchBtn = screen.getByLabelText(/search/i); 20 | 21 | user.type(input, userName); 22 | user.click(searchBtn); 23 | 24 | // async? 25 | expect(mockHistoryPushState).toBeCalledTimes(1); 26 | expect(window.location.href).toContain(`/user/${userName}`); 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import Activities from './Activities'; 2 | import Button from './Button'; 3 | import Error from './Error'; 4 | import Footer from './Footer'; 5 | import Form from './Form'; 6 | import Header from './Header'; 7 | import Loader from './Loader'; 8 | import Logo from './Logo'; 9 | import MaterialTabs from './MaterialTabs'; 10 | import Profile from './Profile'; 11 | import Stats from './Stats'; 12 | import Timeline from './Timeline'; 13 | import TimelineItem from './TimelineItem'; 14 | import Toggle from './Toggle'; 15 | 16 | export { 17 | Activities, 18 | Button, 19 | Error, 20 | Footer, 21 | Form, 22 | Header, 23 | Loader, 24 | Logo, 25 | MaterialTabs, 26 | Profile, 27 | Stats, 28 | Timeline, 29 | TimelineItem, 30 | Toggle 31 | }; 32 | -------------------------------------------------------------------------------- /src/contexts/LanguageContext.js: -------------------------------------------------------------------------------- 1 | // Context for sharing total Languages among components 2 | import React from 'react'; 3 | 4 | export default React.createContext([]); 5 | -------------------------------------------------------------------------------- /src/contexts/ThemeProvider.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ThemeProvider } from 'styled-components'; 3 | 4 | import { useDarkMode } from '../useDarkMode'; 5 | 6 | import { light as LightTheme, dark as DarkTheme } from '../style'; 7 | 8 | const ThemeProviderWrapper = ({ children }) => { 9 | // Custom hook for persistent darkmode 10 | const [theme, setTheme] = useDarkMode(); 11 | 12 | return ( 13 | { 17 | setTheme((state) => (state.id === 'light' ? DarkTheme : LightTheme)); 18 | } 19 | }}> 20 | {children} 21 | 22 | ); 23 | }; 24 | 25 | export default ThemeProviderWrapper; 26 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './components/App'; 4 | 5 | // eslint-disable-next-line react/no-deprecated 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /src/mocks/handlers.ts: -------------------------------------------------------------------------------- 1 | import { rest } from 'msw'; 2 | import { BASE_URL } from 'src/api/base'; 3 | import { mockUserData, mockRepoList, mockActivityData } from 'src/mock.data'; 4 | 5 | export const handlers = [ 6 | rest.get(`${BASE_URL}/users/:userId/events`, (req, res, ctx) => { 7 | return res( 8 | // Respond with a 200 status code 9 | ctx.status(200), 10 | ctx.json(mockActivityData) 11 | ); 12 | }), 13 | rest.get(`${BASE_URL}/users/:userId/repos`, (req, res, ctx) => { 14 | return res( 15 | // Respond with a 200 status code 16 | ctx.status(200), 17 | ctx.json(mockRepoList) 18 | ); 19 | }), 20 | rest.get(`${BASE_URL}/users/:userId`, (req, res, ctx) => { 21 | return res( 22 | // Respond with a 200 status code 23 | ctx.status(200), 24 | ctx.json({ ...mockUserData, login: req.params.userId }) 25 | ); 26 | }) 27 | ]; 28 | -------------------------------------------------------------------------------- /src/mocks/server.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node'; 2 | import { handlers } from './handlers'; 3 | 4 | // This configures a request mocking server with the given request handlers. 5 | export const server = setupServer(...handlers); 6 | -------------------------------------------------------------------------------- /src/pages/Home.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import styled, { ThemeContext } from 'styled-components'; 3 | import { Logo, Form, Toggle } from '../components'; 4 | 5 | const StyledHeader = styled.header` 6 | height: 7rem; 7 | display: flex; 8 | align-items: center; 9 | padding: 1rem 6rem; 10 | justify-content: flex-end; 11 | 12 | & button svg { 13 | font-size: 2rem; 14 | vertical-align: middle; 15 | } 16 | 17 | @media only screen and (max-width: 600px) { 18 | padding: 1rem 2rem; 19 | } 20 | `; 21 | 22 | const Container = styled.div` 23 | display: flex; 24 | flex-direction: column; 25 | width: 100%; 26 | height: calc(100vh - 21rem); 27 | justify-content: center; 28 | align-items: center; 29 | margin-bottom: 7rem; 30 | 31 | @media only screen and (max-width: 600px) { 32 | margin-bottom: 1rem; 33 | } 34 | 35 | & form { 36 | margin-top: 4rem; 37 | 38 | & svg { 39 | font-size: 2rem; 40 | } 41 | } 42 | `; 43 | 44 | const Home = () => { 45 | const { id, setTheme } = useContext(ThemeContext); 46 | 47 | return ( 48 | <> 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ); 58 | }; 59 | 60 | export default Home; 61 | -------------------------------------------------------------------------------- /src/pages/UserProfile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { useParams } from 'react-router-dom'; 4 | import { 5 | Header, 6 | Profile, 7 | MaterialTabs, 8 | Stats, 9 | Timeline, 10 | Activities, 11 | Footer, 12 | Loader, 13 | Error 14 | } from '../components'; 15 | import { useGithubUserData, useLangData, useUserRepos, useActivityData } from '../api/githubAPI'; 16 | import LanguageContext from '../contexts/LanguageContext'; 17 | 18 | const TabSection = styled.section` 19 | padding: 2rem 6rem; 20 | @media only screen and (max-width: 900px) { 21 | padding: 1.5rem 2rem; 22 | } 23 | `; 24 | 25 | const UserProfile = () => { 26 | const params = useParams(); 27 | const username = params.id; 28 | 29 | const [langData, langLoading, langError] = useLangData(username); 30 | const [userData, userLoading, userError] = useGithubUserData(username); 31 | const [repoData, repoLoading, repoError] = useUserRepos(username); 32 | const [activityData, activityLoading, activityError] = useActivityData(username); 33 | 34 | const loading = userLoading || langLoading || repoLoading || activityLoading; 35 | 36 | const error = 37 | userError && 38 | userError.active && 39 | langError && 40 | langError.active && 41 | repoError && 42 | repoError.active && 43 | activityError && 44 | activityError.active; 45 | 46 | if (loading) { 47 | return ( 48 | <> 49 |
    50 | 51 | 52 | ); 53 | } else { 54 | return ( 55 | <> 56 |
    57 |
    58 | {error ? ( 59 | 60 | ) : ( 61 | 62 | 63 | 64 | } 66 | tab2={} 67 | tab3={} 68 | /> 69 | 70 | 71 | )} 72 |
    73 |