├── public ├── _redirects ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json └── index.html ├── netlify.toml ├── src ├── components │ ├── Stats │ │ ├── index.ts │ │ ├── utils.ts │ │ ├── __tests__ │ │ │ └── utils.test.ts │ │ └── Stats.js │ ├── Logo.js │ ├── Loader.js │ ├── App.js │ ├── Button.js │ ├── index.js │ ├── __tests__ │ │ └── Form.test.tsx │ ├── Toggle.js │ ├── Error.js │ ├── Footer.js │ ├── Header.js │ ├── TimelineItem.js │ ├── MaterialTabs.js │ ├── Form.js │ ├── Chart.js │ ├── Timeline.js │ ├── Activities.js │ └── Profile.js ├── assets │ ├── demo.gif │ ├── logo.png │ └── loader.gif ├── contexts │ ├── LanguageContext.js │ └── ThemeProvider.js ├── style │ ├── index.js │ ├── dark.js │ ├── light.js │ └── GlobalStyle.js ├── index.tsx ├── mocks │ ├── server.ts │ └── handlers.ts ├── useDarkMode.js ├── api │ ├── base.js │ └── githubAPI.js ├── pages │ ├── Home.js │ ├── __tests__ │ │ └── UserProfile.test.tsx │ └── UserProfile.js └── types.ts ├── .npmrc ├── setupTest.ts ├── __mocks__ ├── fileMock.js └── gh-polyglot.js ├── .prettierrc ├── cypress ├── .eslintrc.json ├── fixtures │ └── example.json ├── e2e │ └── app.cy.js └── support │ ├── e2e.ts │ └── commands.ts ├── babel.config.js ├── cypress.config.ts ├── .gitignore ├── .github └── workflows │ └── integrate.yaml ├── .eslintrc.json ├── jest.config.ts ├── LICENSE ├── package.json ├── README.md └── tsconfig.json /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish="build" -------------------------------------------------------------------------------- /src/components/Stats/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Stats'; 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # TODO: remove once peer deps error are solved 2 | legacy-peer-deps=true -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khusharth/gitpedia/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khusharth/gitpedia/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khusharth/gitpedia/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khusharth/gitpedia/HEAD/src/assets/demo.gif -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khusharth/gitpedia/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /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/assets/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khusharth/gitpedia/HEAD/src/assets/loader.gif -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = 'test-file-stub'; 3 | -------------------------------------------------------------------------------- /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/style/index.js: -------------------------------------------------------------------------------- 1 | import GlobalStyle from './GlobalStyle'; 2 | import light from './light'; 3 | import dark from './dark'; 4 | 5 | export { GlobalStyle, light, dark }; 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "trailingComma": "none", 7 | "jsxBracketSameLine": true 8 | } 9 | -------------------------------------------------------------------------------- /cypress/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "plugins": ["eslint-plugin-cypress"], 4 | "extends": ["plugin:cypress/recommended"], 5 | "env": {"cypress/global": true} 6 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/style/dark.js: -------------------------------------------------------------------------------- 1 | const theme = { 2 | id: 'dark', 3 | bgColor: '#1E1E2C', 4 | cardColor: '#27293d', 5 | inputColor: '#2e3047', 6 | textColor: '#F5F6FA' 7 | }; 8 | 9 | export default theme; 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/style/light.js: -------------------------------------------------------------------------------- 1 | const theme = { 2 | id: 'light', 3 | bgColor: '#F5F6FA', 4 | cardColor: 'rgb(255,255,255)', 5 | inputColor: 'rgba(238,238,238, 0.8)', 6 | textColor: 'rgb(85, 85, 85)' 7 | }; 8 | 9 | export default theme; 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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/useDarkMode.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { light as LightTheme, dark as DarkTheme } from './style'; 3 | 4 | export const useDarkMode = () => { 5 | const [theme, setTheme] = useState(LightTheme); 6 | 7 | const toggleTheme = () => { 8 | if (theme.id === 'light') { 9 | window.localStorage.setItem('theme', 'dark'); 10 | setTheme(DarkTheme); 11 | } else { 12 | window.localStorage.setItem('theme', 'light'); 13 | setTheme(LightTheme); 14 | } 15 | }; 16 | 17 | useEffect(() => { 18 | const localTheme = window.localStorage.getItem('theme'); 19 | localTheme === 'light' ? localTheme && setTheme(LightTheme) : localTheme && setTheme(DarkTheme); 20 | }, []); 21 | 22 | return [theme, toggleTheme]; 23 | }; 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /__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 | -------------------------------------------------------------------------------- /src/style/GlobalStyle.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | const GlobalStyle = createGlobalStyle` 4 | 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | *, 11 | *::before, 12 | *::after { 13 | box-sizing: inherit; 14 | } 15 | 16 | html { 17 | box-sizing: border-box; 18 | font-size: 62.5%; 19 | 20 | @media only screen and (max-width: 900px) { 21 | font-size: 56%; 22 | } 23 | } 24 | 25 | 26 | body { 27 | font-family: 'Roboto', sans-serif; 28 | font-weight: 400; 29 | font-size: 1.6rem; 30 | line-height: 1.6; 31 | color: ${(p) => p.theme.textColor}; 32 | /* background-color: #F0F1F6; */ 33 | background-color: #F5F6FA; 34 | background-color: ${(p) => p.theme.bgColor}; 35 | } 36 | 37 | ul { 38 | list-style: none; 39 | } 40 | `; 41 | 42 | export default GlobalStyle; 43 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/pages/__tests__/UserProfile.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 3 | import { render, screen } from '@testing-library/react'; 4 | import user from '@testing-library/user-event'; 5 | import UserProfile from '../UserProfile'; 6 | import ThemeProviderWrapper from '../../contexts/ThemeProvider'; 7 | 8 | import { mockUserData } from 'src/mock.data'; 9 | 10 | test('render the expected user profile when user searches a user from the user profile page', async () => { 11 | const userName = 'khusharth'; 12 | 13 | window.history.pushState({ id: 'hello' }, '', `/user/${userName}`); 14 | 15 | render( 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | 23 | // verify intro card data 24 | await screen.findByLabelText(/github profile/i); 25 | await screen.findByText(mockUserData.name); 26 | 27 | // verify stats card is shown 28 | await screen.findByText(/Joined github on/); 29 | // had to create a regex as there is spacing in between 30 | const regexPattern = `created ${mockUserData.public_repos} projects`; 31 | await screen.findByText(new RegExp(regexPattern)); 32 | 33 | // stats tab is visible and active 34 | const activeTab = await screen.findByRole('tab', { selected: true }); 35 | expect(activeTab).toHaveTextContent('Stats'); 36 | 37 | await screen.findByText(/Repositories/); 38 | await screen.findByText(/Total Stars/); 39 | 40 | // click on Timeline tab 41 | const tab2 = await screen.getByRole('tab', { name: 'Timeline' }); 42 | user.click(tab2); 43 | expect(screen.getByRole('tab', { selected: true })).toHaveTextContent('Timeline'); 44 | }); 45 | -------------------------------------------------------------------------------- /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/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/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/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 |