├── .eslintignore ├── src ├── react-app-env.d.ts ├── assets │ ├── avatar.png │ ├── no-found.png │ ├── company.svg │ ├── back.svg │ ├── location.svg │ ├── code.svg │ ├── search.svg │ ├── star.svg │ ├── branch.svg │ ├── repository.svg │ ├── followers.svg │ ├── following.svg │ ├── logoh.svg │ └── logov.svg ├── setupTests.ts ├── index.css ├── components │ ├── NotFounded │ │ ├── styles.ts │ │ └── index.tsx │ ├── Input │ │ └── index.ts │ ├── Button │ │ └── index.ts │ ├── Header │ │ ├── styles.ts │ │ └── index.tsx │ ├── Spinner │ │ └── index.tsx │ ├── Repository │ │ ├── styles.ts │ │ └── index.tsx │ └── UserInfo │ │ ├── styles.ts │ │ └── index.tsx ├── reportWebVitals.ts ├── interfaces │ └── index.ts ├── pages │ ├── Home │ │ ├── styles.ts │ │ └── index.tsx │ └── User │ │ ├── styles.ts │ │ ├── utility.ts │ │ └── index.tsx ├── App.tsx ├── services │ └── api.ts └── index.tsx ├── public ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json ├── favicon.svg └── index.html ├── .prettierrc ├── .gitignore ├── tsconfig.json ├── README.md ├── .eslintrc.json └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/**/*.ts -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C0lliNN/GithubSearch/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C0lliNN/GithubSearch/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/assets/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C0lliNN/GithubSearch/HEAD/src/assets/avatar.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true, 4 | "printWidth": 80 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/no-found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C0lliNN/GithubSearch/HEAD/src/assets/no-found.png -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /.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 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | todo.txt -------------------------------------------------------------------------------- /src/assets/company.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/NotFounded/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | `; 8 | 9 | export const Title = styled.h4` 10 | color: ${(props) => props.theme.secondary}; 11 | font-size: ${(props) => props.theme.fontSizeExtraBig}; 12 | `; 13 | 14 | export const Image = styled.img` 15 | display: block; 16 | width: 150px; 17 | height: 150px; 18 | margin: auto; 19 | `; 20 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | export interface User { 3 | name: string; 4 | avatar_url: string; 5 | twitter_username: string; 6 | public_repos: number; 7 | location: string; 8 | company: string; 9 | followers: number; 10 | following: number; 11 | } 12 | 13 | export interface Repository { 14 | name: string; 15 | description: string; 16 | stars: number; 17 | forks: number; 18 | stargazers_count: number; 19 | language: string; 20 | updated_at: string; 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/Home/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.section` 4 | align-self: center; 5 | width: 300px; 6 | margin: auto; 7 | `; 8 | 9 | export const LogoContainer = styled.div` 10 | width: 100%; 11 | text-align: center; 12 | `; 13 | 14 | export const FormContainer = styled.form` 15 | margin-top: 50px; 16 | display: flex; 17 | align-items: stretch; 18 | flex-direction: column; 19 | 20 | & button { 21 | margin-top: 40px; 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /src/components/Input/index.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Input = styled.input` 4 | outline: none; 5 | border: none; 6 | border-bottom: 5px solid ${(props) => props.theme.primary}; 7 | background-color: transparent; 8 | color: ${(props) => props.theme.secondary}; 9 | padding: 12px 20px; 10 | box-sizing: border-box; 11 | font-size: ${(props) => props.theme.fontSizeBig}; 12 | text-align: center; 13 | 14 | &::placeholder { 15 | color: #535353; 16 | } 17 | `; 18 | 19 | export default Input; 20 | -------------------------------------------------------------------------------- /src/components/NotFounded/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Fade, Slide } from 'react-awesome-reveal'; 3 | import { Container, Image, Title } from './styles'; 4 | import notFounded from '../../assets/no-found.png'; 5 | 6 | export default function NotFounded() { 7 | return ( 8 | 9 | 10 | 11 | User Not Founded! 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/User/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.section` 4 | width: 80%; 5 | min-width: 300px; 6 | align-self: flex-start; 7 | padding: 30px 0; 8 | box-sizing: border-box; 9 | @media (min-width: ${(props) => props.theme.mdBreakPoint}) { 10 | padding: 40px 0; 11 | } 12 | `; 13 | 14 | export const Repositories = styled.ul` 15 | list-style: none; 16 | padding: 0; 17 | display: flex; 18 | flex-wrap: wrap; 19 | justify-content: center; 20 | align-items: stretch; 21 | gap: 20px; 22 | `; 23 | -------------------------------------------------------------------------------- /src/components/Button/index.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Button = styled.button` 4 | background-color: ${(props) => props.theme.primary}; 5 | border: none; 6 | outline: none; 7 | padding: 12px 20px; 8 | box-sizing: border-box; 9 | border-radius: 5px; 10 | color: #fff; 11 | font-size: ${(props) => props.theme.fontSizeBig}; 12 | font-weight: ${(props) => props.theme.fontWeightBold}; 13 | cursor: pointer; 14 | transition: all 0.2s ease-in-out; 15 | 16 | &:hover { 17 | background-color: #7b46c0; 18 | } 19 | `; 20 | 21 | export default Button; 22 | -------------------------------------------------------------------------------- /src/assets/back.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 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 | -------------------------------------------------------------------------------- /src/assets/location.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Header/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.header` 4 | & > div { 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | } 9 | `; 10 | 11 | export const BackButton = styled.button` 12 | background-color: transparent; 13 | border: none; 14 | outline: none; 15 | cursor: pointer; 16 | width: 40px; 17 | height: 40px; 18 | padding: 0; 19 | border-radius: 100%; 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | transition: all 0.3s ease-in-out; 24 | &:hover { 25 | background-color: rgba(255, 255, 255, 0.05); 26 | } 27 | `; 28 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch } from 'react-router'; 3 | import styled from 'styled-components'; 4 | import Home from './pages/Home'; 5 | import User from './pages/User'; 6 | 7 | const Container = styled.main` 8 | min-height: 100vh; 9 | width: 100%; 10 | display: flex; 11 | justify-content: center; 12 | background-color: ${(props) => props.theme.background}; 13 | `; 14 | 15 | function App() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /src/assets/code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Search 2 | 3 | A Web application developed in React/Typescript that allows users to search for Github Profiles and show them with an amazing User Experience. 4 | 5 | ![Login Page](https://i.imgur.com/JcdbhDr.png) 6 | ![User Page](https://i.imgur.com/heBynZA.png) 7 | 8 | ### [Demo](https://c0llinn.github.io/GithubSearch) 9 | 10 | ## Tech 11 | 12 | * React 13 | * Typescript 14 | * React Router 15 | * React Awesome Reveal 16 | * Styled Components 17 | * React Query 18 | 19 | ## How to use 20 | 21 | After you clone the project, you can run: 22 | 23 | ### `yarn install && yarn start` 24 | 25 | ### Inspiration 26 | 27 | I inspired myself in the [GitHub Search](https://github.com/Diegooliveyra/Github-Search) repository from [Diego Oliveira](https://github.com/Diegooliveyra) 28 | -------------------------------------------------------------------------------- /src/components/Spinner/index.tsx: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | 3 | const load = keyframes` 4 | 0% { 5 | transform: rotate(0deg); 6 | } 7 | 100% { 8 | transform: rotate(360deg); 9 | } 10 | `; 11 | 12 | const Spinner = styled.div` 13 | margin: 60px auto; 14 | font-size: 10px; 15 | position: relative; 16 | text-indent: -9999em; 17 | border-top: 1.1em solid rgba(135, 82, 204, 0.2); 18 | border-right: 1.1em solid rgba(135, 82, 204, 0.2); 19 | border-bottom: 1.1em solid rgba(135, 82, 204, 0.2); 20 | border-left: 1.1em solid #8752cc; 21 | transform: translateZ(0); 22 | animation: ${load} 1.1s infinite linear; 23 | border-radius: 50%; 24 | width: 10em; 25 | height: 10em; 26 | &:after { 27 | border-radius: 50%; 28 | width: 10em; 29 | height: 10em; 30 | } 31 | `; 32 | 33 | export default Spinner; 34 | -------------------------------------------------------------------------------- /src/services/api.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | import axios from 'axios'; 3 | 4 | const api = axios.create({ 5 | baseURL: 'https://api.github.com', 6 | headers: { 7 | Accept: 'application/vnd.github.v3+json', 8 | }, 9 | }); 10 | 11 | function getUser(username: string) { 12 | return api.get(`/users/${username}`); 13 | } 14 | 15 | function getRepositories(username: string) { 16 | return api.get(`/users/${username}/repos`, { 17 | params: { 18 | per_page: 30, 19 | }, 20 | }); 21 | } 22 | 23 | function getStarred(username: string) { 24 | return api.get(`/users/${username}/starred`, { 25 | params: { 26 | per_page: 1, 27 | page: 2, 28 | }, 29 | }); 30 | } 31 | 32 | export function getUserData(username: string) { 33 | return axios.all([ 34 | getUser(username), 35 | getRepositories(username), 36 | getStarred(username), 37 | ]); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useHistory } from 'react-router'; 3 | import { Link } from 'react-router-dom'; 4 | import { Fade, Slide } from 'react-awesome-reveal'; 5 | import { BackButton, Container } from './styles'; 6 | import logoh from '../../assets/logoh.svg'; 7 | import back from '../../assets/back.svg'; 8 | 9 | export default function Header() { 10 | const history = useHistory(); 11 | 12 | function handleBack() { 13 | history.goBack(); 14 | } 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | Github Search 22 | 23 | 24 | Back 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["plugin:react/recommended", "airbnb", "prettier"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "ecmaVersion": 12, 13 | "sourceType": "module" 14 | }, 15 | "plugins": ["react", "@typescript-eslint"], 16 | "rules": { 17 | "react/jsx-filename-extension": [ 18 | 1, 19 | { "extensions": [".js", ".jsx", ".ts", ".tsx"] } 20 | ], 21 | "import/extensions": [ 22 | "error", 23 | "ignorePackages", 24 | { 25 | "js": "never", 26 | "jsx": "never", 27 | "ts": "never", 28 | "tsx": "never" 29 | } 30 | ], 31 | "no-use-before-define": "off", 32 | "@typescript-eslint/no-use-before-define": ["error"], 33 | "camelcase": "error", 34 | "react/prop-types": "off" 35 | }, 36 | "settings": { 37 | "import/resolver": { 38 | "node": { 39 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/assets/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { ThemeProvider } from 'styled-components'; 5 | import { QueryClient, QueryClientProvider } from 'react-query'; 6 | import './index.css'; 7 | import App from './App'; 8 | import reportWebVitals from './reportWebVitals'; 9 | 10 | const theme = { 11 | background: '#232324', 12 | dark: '#201F1F', 13 | primary: '#8752CC', 14 | secondary: '#B2B2B2', 15 | fontWeightNormal: 400, 16 | fontWeightBold: 700, 17 | fontSizeSmall: '14px', 18 | fontsizeMedium: '16px', 19 | fontSizeBig: '18px', 20 | fontSizeExtraBig: '24px', 21 | mdBreakPoint: '768px', 22 | }; 23 | 24 | const queryClient = new QueryClient(); 25 | 26 | ReactDOM.render( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | , 36 | document.getElementById('root'), 37 | ); 38 | 39 | // If you want to start measuring performance in your app, pass a function 40 | // to log results (for example: reportWebVitals(console.log)) 41 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 42 | reportWebVitals(); 43 | -------------------------------------------------------------------------------- /src/components/Repository/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.li` 4 | width: 282px; 5 | 6 | & .content { 7 | display: flex; 8 | align-items: start; 9 | justify-content: space-between; 10 | flex-direction: column; 11 | background-color: ${(props) => props.theme.dark}; 12 | height: 100%; 13 | padding: 20px 15px 15px 15px; 14 | box-sizing: border-box; 15 | } 16 | & > div { 17 | height: 100%; 18 | } 19 | & > div > div { 20 | height: 100%; 21 | } 22 | `; 23 | 24 | export const Title = styled.h4` 25 | font-weight: bold; 26 | font-size: ${(props) => props.theme.fontSizeBig}; 27 | color: ${(props) => props.theme.secondary}; 28 | margin-bottom: 8px; 29 | margin-top: 0; 30 | `; 31 | 32 | export const Description = styled.p` 33 | font-weight: normal; 34 | font-size: ${(props) => props.theme.fontSizeSmall}; 35 | color: ${(props) => props.theme.secondary}; 36 | `; 37 | 38 | export const Info = styled.div` 39 | list-style: none; 40 | margin: 0; 41 | display: flex; 42 | align-items: center; 43 | 44 | & div { 45 | display: flex; 46 | align-items: center; 47 | color: ${(props) => props.theme.secondary}; 48 | margin-right: 15px; 49 | &:last-child { 50 | margin-right: 0; 51 | } 52 | } 53 | & img { 54 | margin-right: 5px; 55 | } 56 | `; 57 | -------------------------------------------------------------------------------- /src/components/Repository/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Fade, Slide } from 'react-awesome-reveal'; 3 | import { Repository as IRepository } from '../../interfaces'; 4 | import { Container, Title, Description, Info } from './styles'; 5 | import star from '../../assets/star.svg'; 6 | import branch from '../../assets/branch.svg'; 7 | import code from '../../assets/code.svg'; 8 | 9 | interface Props { 10 | repository: IRepository; 11 | } 12 | 13 | export default function Repository(props: Props) { 14 | const { repository } = props; 15 | return ( 16 | 17 | 18 | 19 |
20 |
21 | {repository.name} 22 | {repository.description} 23 |
24 | 25 |
26 | Stars: 27 | {repository.stargazers_count} 28 |
29 |
30 | Forks: 31 | {repository.forks} 32 |
33 |
34 | Language: 35 | {repository.language} 36 |
37 |
38 |
39 |
40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/assets/branch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent, FormEvent, useState } from 'react'; 2 | import { useHistory } from 'react-router'; 3 | import { Slide, Fade } from 'react-awesome-reveal'; 4 | 5 | import Button from '../../components/Button'; 6 | import { Container, LogoContainer, FormContainer } from './styles'; 7 | import logov from '../../assets/logov.svg'; 8 | import Input from '../../components/Input'; 9 | 10 | export default function Home() { 11 | const [username, setUsername] = useState(''); 12 | const history = useHistory(); 13 | 14 | function handleUsernameChange(e: ChangeEvent) { 15 | const el = e.target as HTMLInputElement; 16 | setUsername(el.value); 17 | } 18 | 19 | function handleSearch(e: FormEvent) { 20 | e.preventDefault(); 21 | history.push(`/users/${username}`); 22 | } 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | Github Search 30 | 31 | 32 | 33 | 34 | 35 | 36 | 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/pages/User/utility.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | import { Repository, User } from '../../interfaces'; 3 | 4 | export function getUserFromResponse(response: AxiosResponse): User { 5 | return response.data; 6 | } 7 | 8 | export function getRepositoriesFromResponse( 9 | response: AxiosResponse, 10 | ): Repository[] { 11 | return response.data; 12 | } 13 | 14 | function parseLinkHeader(header: string) { 15 | if (!header || !header.length) { 16 | return null; 17 | } 18 | 19 | const parts = header.split(','); 20 | const links = {} as { [index: string]: string }; 21 | 22 | for (let i = 0; i < parts.length; i += 1) { 23 | const section = parts[i].split(';'); 24 | if (section.length !== 2) { 25 | throw new Error("section could not be split on ';'"); 26 | } 27 | const url = section[0].replace(/<(.*)>/, '$1').trim(); 28 | const name = section[1].replace(/rel="(.*)"/, '$1').trim(); 29 | links[name] = url; 30 | } 31 | return links; 32 | } 33 | 34 | export function getStarsCountFromResponse(response: AxiosResponse): string { 35 | const parsedLinks = parseLinkHeader(response?.headers.link); 36 | 37 | let totalStars = '0'; 38 | 39 | if (parsedLinks) { 40 | const params = new URLSearchParams(parsedLinks.last); 41 | totalStars = params.get('page') as string; 42 | } 43 | 44 | return totalStars; 45 | } 46 | 47 | export function sortRepositories(a: Repository, b: Repository): number { 48 | if (a.updated_at < b.updated_at) { 49 | return 1; 50 | } 51 | if (a.updated_at > b.updated_at) { 52 | return -1; 53 | } 54 | return 0; 55 | } 56 | 57 | export function filterRepositories(repository: Repository): boolean { 58 | return !!repository.language; 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-search", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://c0llinn.github.io/GithubSearch", 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.11.4", 8 | "@testing-library/react": "^11.1.0", 9 | "@testing-library/user-event": "^12.1.10", 10 | "@types/jest": "^26.0.15", 11 | "@types/node": "^12.0.0", 12 | "@types/react": "^17.0.0", 13 | "axios": "^0.21.1", 14 | "react": "^17.0.1", 15 | "react-awesome-reveal": "^3.7.0", 16 | "react-dom": "^17.0.1", 17 | "react-query": "^3.7.1", 18 | "react-router": "^5.2.0", 19 | "react-router-dom": "^5.2.0", 20 | "react-scripts": "4.0.2", 21 | "styled-components": "^5.2.1", 22 | "typescript": "^4.1.2", 23 | "web-vitals": "^1.0.1" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject", 30 | "predeploy": "yarn build", 31 | "deploy": "gh-pages -d build" 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | }, 45 | "devDependencies": { 46 | "@types/react-dom": "^17.0.0", 47 | "@types/react-router-dom": "^5.1.7", 48 | "@types/styled-components": "^5.1.7", 49 | "@typescript-eslint/eslint-plugin": "^4.11.1", 50 | "eslint": "^7.11.0", 51 | "eslint-config-airbnb": "^18.2.1", 52 | "eslint-config-prettier": "^7.1.0", 53 | "eslint-plugin-react": "^7.21.5", 54 | "gh-pages": "^3.1.0", 55 | "prettier": "^2.2.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/assets/repository.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 28 | 32 | Github Search 33 | 34 | 35 | 36 |
37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/pages/User/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AxiosResponse } from 'axios'; 3 | import { useQuery } from 'react-query'; 4 | import { useParams } from 'react-router'; 5 | import { getUserData } from '../../services/api'; 6 | import { 7 | filterRepositories, 8 | getRepositoriesFromResponse, 9 | getStarsCountFromResponse, 10 | getUserFromResponse, 11 | sortRepositories, 12 | } from './utility'; 13 | import Header from '../../components/Header'; 14 | import { Container, Repositories } from './styles'; 15 | import UserInfo from '../../components/UserInfo'; 16 | import Spinner from '../../components/Spinner'; 17 | import Repository from '../../components/Repository'; 18 | import NotFounded from '../../components/NotFounded'; 19 | 20 | interface Params { 21 | username: string; 22 | } 23 | 24 | export default function User() { 25 | const { username } = useParams(); 26 | 27 | const { data: responses, isLoading, error } = useQuery( 28 | ['getUserData', username], 29 | () => getUserData(username), 30 | { retry: false, refetchOnWindowFocus: false }, 31 | ); 32 | 33 | if (isLoading) { 34 | return ; 35 | } 36 | 37 | if (error) { 38 | return ( 39 | 40 |
41 | 42 | 43 | ); 44 | } 45 | 46 | const [ 47 | userResponse, 48 | repositoriesResponse, 49 | starredResponse, 50 | ] = responses as Array; 51 | 52 | const user = getUserFromResponse(userResponse); 53 | const repositories = getRepositoriesFromResponse(repositoriesResponse); 54 | const starsCount = getStarsCountFromResponse(starredResponse); 55 | 56 | return ( 57 | 58 |
59 | 60 | 61 | {repositories 62 | .filter(filterRepositories) 63 | .sort(sortRepositories) 64 | .map((repository) => ( 65 | 66 | ))} 67 | 68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/components/UserInfo/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | margin-top: 35px; 5 | margin-bottom: 20px; 6 | 7 | @media (min-width: ${(props) => props.theme.mdBreakPoint}) { 8 | margin-top: 80px; 9 | margin-bottom: 100px; 10 | } 11 | & > div { 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | flex-direction: column; 16 | @media (min-width: ${(props) => props.theme.mdBreakPoint}) { 17 | flex-direction: row; 18 | } 19 | } 20 | `; 21 | 22 | export const Info = styled.div` 23 | margin: 35px 0px; 24 | width: 220px; 25 | 26 | & h2 { 27 | color: ${(props) => props.theme.primary}; 28 | margin: auto; 29 | } 30 | & h3 { 31 | color: ${(props) => props.theme.secondary}; 32 | font-weight: ${(props) => props.theme.fontWeightNormal}; 33 | font-size: ${(props) => props.theme.fontsizeMedium}; 34 | margin: 0; 35 | margin-top: 5px; 36 | } 37 | & .details { 38 | display: flex; 39 | align-items: center; 40 | flex-wrap: wrap; 41 | margin-top: 20px; 42 | 43 | & div { 44 | display: flex; 45 | align-items: center; 46 | color: ${(props) => props.theme.secondary}; 47 | margin-right: 15px; 48 | margin-bottom: 5px; 49 | &:last-child { 50 | margin-right: 0; 51 | } 52 | } 53 | & img { 54 | margin-right: 5px; 55 | } 56 | } 57 | @media (min-width: ${(props) => props.theme.mdBreakPoint}) { 58 | margin: 0 35px; 59 | } 60 | `; 61 | 62 | export const Repositories = styled.div` 63 | background-color: ${(props) => props.theme.dark}; 64 | padding: 15px 20px; 65 | 66 | & h3 { 67 | font-weight: ${(props) => props.theme.fontWeightNormal}; 68 | font-size: ${(props) => props.theme.fontsizeMedium}; 69 | color: ${(props) => props.theme.secondary}; 70 | text-align: center; 71 | margin: 0; 72 | margin-bottom: 10px; 73 | } 74 | 75 | & div { 76 | display: flex; 77 | align-items: center; 78 | justify-content: center; 79 | color: ${(props) => props.theme.secondary}; 80 | font-size: ${(props) => props.theme.fontSizeExtraBig}; 81 | 82 | & img { 83 | margin-right: 10px; 84 | } 85 | } 86 | `; 87 | 88 | export const Photo = styled.img` 89 | width: 146px; 90 | height: 146px; 91 | border-radius: 100%; 92 | `; 93 | -------------------------------------------------------------------------------- /src/components/UserInfo/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-one-expression-per-line */ 2 | import React from 'react'; 3 | import { Fade, Slide } from 'react-awesome-reveal'; 4 | import { User } from '../../interfaces'; 5 | import { Container, Photo, Info, Repositories } from './styles'; 6 | import repository from '../../assets/repository.svg'; 7 | import location from '../../assets/location.svg'; 8 | import company from '../../assets/company.svg'; 9 | import followers from '../../assets/followers.svg'; 10 | import following from '../../assets/following.svg'; 11 | import stars from '../../assets/star.svg'; 12 | 13 | interface Props { 14 | user: User; 15 | startsCount: string; 16 | } 17 | 18 | export default function UserInfo(props: Props) { 19 | const { user, startsCount } = props; 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 |

{user.name}

27 | {user.twitter_username &&

@{user.twitter_username}

} 28 |
29 | {user.location && ( 30 |
31 | Location: 32 | {user.location} 33 |
34 | )} 35 | {user.company && ( 36 |
37 | Company: 38 | {user.company} 39 |
40 | )} 41 |
42 | Followers: 43 | {user.followers} 44 |
45 |
46 | Following: 47 | {user.following} 48 |
49 |
50 | Stars: 51 | {startsCount} 52 |
53 |
54 |
55 | 56 |

Total Repositories

57 |
58 | 59 | {user.public_repos} 60 |
61 |
62 |
63 |
64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/assets/followers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/following.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/logoh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/logov.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | --------------------------------------------------------------------------------