├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.js ├── components │ └── Container │ │ └── index.js ├── helpers │ └── colorContrast.js ├── index.js ├── pages │ ├── Main │ │ ├── Main.js │ │ ├── MainStyles.js │ │ └── package.json │ └── Repository │ │ ├── Repository.js │ │ ├── RepositoryStyles.js │ │ └── package.json ├── routes.js ├── services │ └── api.js └── styles │ └── global.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | }, 6 | extends: [ 7 | 'airbnb', 8 | 'prettier', 9 | 'prettier/react' 10 | ], 11 | globals: { 12 | Atomics: 'readonly', 13 | SharedArrayBuffer: 'readonly', 14 | }, 15 | parser: 'babel-eslint', 16 | parserOptions: { 17 | ecmaFeatures: { 18 | jsx: true, 19 | }, 20 | ecmaVersion: 2018, 21 | sourceType: 'module', 22 | }, 23 | plugins: [ 24 | 'react', 25 | 'prettier', 26 | ], 27 | rules: { 28 | 'prettier/prettier': 'error', 29 | 'react/jsx-filename-extension': [ 30 | 'warn', 31 | {extensions: ['.jsx', '.js']} 32 | ], 33 | 'import/prefer-default-export': 'off', 34 | 'import/no-extraneous-dependencies': context => [ 35 | 'error', 36 | { 37 | devDependencies: true, 38 | packageDir: [context.getFilename(), __dirname] 39 | } 40 | ], 41 | "no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true }] 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Luke Morales 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 | React GitHub Repo List 3 |
4 | React GitHub Repositories List 5 |

6 | 7 |

8 | List your favorite GitHub repositories and see information and issues for each of them. 9 |

10 |

11 | GitHub top language 12 | 13 | GitHub language count 14 | 15 | 16 | Codacy grade 17 | 18 | 19 | Repository size 20 | 21 | GitHub last commit 22 | 23 | 24 | 25 | Repository issues 26 | 27 | 28 | GitHub 29 |

30 | 31 |

32 | Technologies   |    33 | How To Use   |    34 | License 35 |

36 | 37 | ![App Screenshot](https://res.cloudinary.com/lukemorales/image/upload/v1562471845/readme_logos/github-repo-react-screenshot_n8yg2f.jpg) 38 |

39 | 40 | Demo on Heroku 41 | 42 |

43 | 44 | ## :rocket: Technologies 45 | 46 | This project was developed at the [RocketSeat GoStack Bootcamp](https://rocketseat.com.br/bootcamp) with the following technologies: 47 | 48 | - [ReactJS](https://reactjs.org/) 49 | - [React Router v4](https://github.com/ReactTraining/react-router) 50 | - [styled-components](https://www.styled-components.com/) 51 | - [GitHub REST API v3](https://developer.github.com/v3/) 52 | - [VS Code][vc] with [EditorConfig][vceditconfig] and [ESLint][vceslint] 53 | 54 | ## :information_source: How To Use 55 | 56 | To clone and run this application, you'll need [Git](https://git-scm.com), [Node.js v10.16][nodejs] or higher + [Yarn v1.13][yarn] or higher installed on your computer. From your command line: 57 | 58 | ```bash 59 | # Clone this repository 60 | $ git clone https://github.com/lukemorales/react-github-repo-list 61 | 62 | # Go into the repository 63 | $ cd react-github-repo-list 64 | 65 | # Install dependencies 66 | $ yarn install 67 | 68 | # Run the app 69 | $ yarn start 70 | ``` 71 | 72 | ## :memo: License 73 | This project is under the MIT license. See the [LICENSE](https://github.com/lukemorales/react-github-repo-list/blob/master/LICENSE) for more information. 74 | 75 | --- 76 | 77 | Made with ♥ by Luke Morales :wave: [Get in touch!](https://www.linkedin.com/in/lukemorales/) 78 | 79 | [nodejs]: https://nodejs.org/ 80 | [yarn]: https://yarnpkg.com/ 81 | [vc]: https://code.visualstudio.com/ 82 | [vceditconfig]: https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig 83 | [vceslint]: https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modulo05", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.19.0", 7 | "prop-types": "^15.7.2", 8 | "react": "^16.8.6", 9 | "react-dom": "^16.8.6", 10 | "react-icons": "^3.7.0", 11 | "react-router-dom": "^5.0.1", 12 | "react-scripts": "3.0.1", 13 | "styled-components": "^4.3.2" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "browserslist": { 22 | "production": [ 23 | ">0.2%", 24 | "not dead", 25 | "not op_mini all" 26 | ], 27 | "development": [ 28 | "last 1 chrome version", 29 | "last 1 firefox version", 30 | "last 1 safari version" 31 | ] 32 | }, 33 | "devDependencies": { 34 | "babel-eslint": "10.0.1", 35 | "eslint": "^5.16.0", 36 | "eslint-config-airbnb": "^17.1.1", 37 | "eslint-config-prettier": "^6.0.0", 38 | "eslint-plugin-import": "^2.18.0", 39 | "eslint-plugin-jsx-a11y": "^6.2.3", 40 | "eslint-plugin-prettier": "^3.1.0", 41 | "eslint-plugin-react": "^7.14.2", 42 | "prettier": "^1.18.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukemorales/react-github-repo-list/0bbf8525204425d6ab616d3a5df76cd00922c5c2/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | GitHub Repo List 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Routes from './routes'; 3 | 4 | import GlobalStyle from './styles/global'; 5 | 6 | function App() { 7 | return ( 8 | <> 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /src/components/Container/index.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Container = styled.div` 4 | max-width: 700px; 5 | background: #fff; 6 | border-radius: 4px; 7 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); 8 | padding: 30px; 9 | margin: 80px auto; 10 | position: relative; 11 | 12 | & > h1 { 13 | font-size: 24px; 14 | text-align: center; 15 | color: #534974; 16 | } 17 | 18 | @media (max-width: 600px) { 19 | margin-top: 0; 20 | border-radius: 0; 21 | } 22 | `; 23 | 24 | export const Icon = styled.h2` 25 | position: absolute; 26 | left: 50%; 27 | bottom: -40px; 28 | transform: translateX(-50%); 29 | background: white; 30 | color: #7159c1; 31 | width: 80px; 32 | height: 80px; 33 | font-size: 48px; 34 | display: flex; 35 | align-items: center; 36 | justify-content: center; 37 | border-radius: 50%; 38 | box-shadow: 0 12px 10px -4px rgba(25, 10, 74, 0.23); 39 | `; 40 | 41 | export default Container; 42 | -------------------------------------------------------------------------------- /src/helpers/colorContrast.js: -------------------------------------------------------------------------------- 1 | // function from https://stackoverflow.com/questions/35969656/how-can-i-generate-the-opposite-color-according-to-current-color 2 | export default function colorContrast(color) { 3 | let hex = color; 4 | if (hex.indexOf('#') === 0) { 5 | hex = hex.slice(1); 6 | } 7 | // convert 3-digit hex to 6-digits. 8 | if (hex.length === 3) { 9 | hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; 10 | } 11 | if (hex.length !== 6) { 12 | throw new Error('Invalid HEX color.'); 13 | } 14 | const r = parseInt(hex.slice(0, 2), 16); 15 | const g = parseInt(hex.slice(2, 4), 16); 16 | const b = parseInt(hex.slice(4, 6), 16); 17 | // http://stackoverflow.com/a/3943023/112731 18 | return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? '#333' : '#fff'; 19 | } 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /src/pages/Main/Main.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { FaGithubAlt, FaPlus, FaSpinner, FaTrash } from 'react-icons/fa'; 4 | import api from '../../services/api'; 5 | 6 | import { Form, SubmitButton, List, ErrorMessage } from './MainStyles'; 7 | import Container, { Icon } from '../../components/Container'; 8 | 9 | class Main extends Component { 10 | state = { 11 | newRepo: '', 12 | repositories: [ 13 | { 14 | name: 'facebook/react', 15 | owner: { 16 | name: 'facebook', 17 | avatar_url: 'https://avatars3.githubusercontent.com/u/69631?v=4', 18 | }, 19 | }, 20 | ], 21 | loading: false, 22 | error: false, 23 | errorMessage: '', 24 | }; 25 | 26 | componentDidMount() { 27 | const repositories = localStorage.getItem('repositories'); 28 | 29 | repositories && this.setState({ repositories: JSON.parse(repositories) }); 30 | } 31 | 32 | componentDidUpdate(_, prevState) { 33 | const { repositories } = this.state; 34 | 35 | prevState.repositories !== repositories && 36 | localStorage.setItem('repositories', JSON.stringify(repositories)); 37 | } 38 | 39 | handleInputChange = e => { 40 | this.setState({ newRepo: e.target.value }); 41 | }; 42 | 43 | handleSubmit = async e => { 44 | e.preventDefault(); 45 | 46 | this.setState({ loading: true, error: false }); 47 | 48 | try { 49 | const { newRepo, repositories } = this.state; 50 | 51 | if (newRepo === '') throw new Error('You need to inform one repository'); 52 | 53 | const response = await api.get(`/repos/${newRepo}`); 54 | 55 | const data = { 56 | name: response.data.full_name, 57 | owner: { 58 | name: response.data.owner.login, 59 | avatar_url: response.data.owner.avatar_url, 60 | }, 61 | }; 62 | 63 | const hasRepo = repositories.find( 64 | repo => repo.name.toLowerCase() === data.name.toLowerCase() 65 | ); 66 | 67 | if (hasRepo) throw new Error('Duplicated Repository'); 68 | 69 | this.setState({ 70 | repositories: [...repositories, data], 71 | newRepo: '', 72 | errorMessage: '', 73 | }); 74 | } catch (Error) { 75 | this.setState({ 76 | error: true, 77 | errorMessage: 78 | Error.message === 'Request failed with status code 404' 79 | ? 'Repository not found' 80 | : Error.message, 81 | }); 82 | } finally { 83 | this.setState({ loading: false }); 84 | } 85 | }; 86 | 87 | handleDelete = repo => { 88 | const { repositories } = this.state; 89 | this.setState({ 90 | repositories: repositories.filter( 91 | repository => repository.name !== repo.name 92 | ), 93 | }); 94 | }; 95 | 96 | render() { 97 | const { newRepo, loading, repositories, error, errorMessage } = this.state; 98 | 99 | return ( 100 | 101 | 102 | 103 | 104 | 105 |

GitHub Repositories

106 | 107 |
108 | 114 | 115 | {loading ? ( 116 | 117 | ) : ( 118 | 119 | )} 120 | 121 |
122 | 123 | {errorMessage && {errorMessage}} 124 | 125 | 126 | {repositories.map(repo => ( 127 |
  • 128 |
    129 | 130 | {repo.owner.name} 131 | {repo.name} 132 | 133 |
    134 | 137 |
  • 138 | ))} 139 |
    140 |
    141 | ); 142 | } 143 | } 144 | 145 | export default Main; 146 | -------------------------------------------------------------------------------- /src/pages/Main/MainStyles.js: -------------------------------------------------------------------------------- 1 | import styled, { keyframes, css } from 'styled-components'; 2 | 3 | export const Form = styled.form` 4 | margin-top: 30px; 5 | display: flex; 6 | flex-direction: row; 7 | 8 | input { 9 | flex: 1; 10 | border: solid ${props => (props.error ? '2px #e41111' : '1px #eee')}; 11 | padding: 10px 15px; 12 | border-radius: 4px; 13 | font-size: 16px; 14 | } 15 | `; 16 | 17 | const rotate = keyframes` 18 | from { 19 | transform: rotate(0deg) 20 | } 21 | 22 | to { 23 | transform: rotate(360deg) 24 | } 25 | `; 26 | 27 | export const SubmitButton = styled.button.attrs(props => ({ 28 | type: 'submit', 29 | disabled: props.loading || props.empty, 30 | }))` 31 | background: #7159c1; 32 | border: 0; 33 | padding: 0 15px; 34 | margin-left: 10px; 35 | border-radius: 4px; 36 | 37 | display: flex; 38 | align-items: center; 39 | justify-content: center; 40 | 41 | &[disabled] { 42 | cursor: not-allowed; 43 | background: rgba(113, 89, 193, 0.2); 44 | } 45 | 46 | ${props => 47 | props.loading && 48 | css` 49 | svg { 50 | animation: ${rotate} 2s linear infinite; 51 | color: #7159c1 !important; 52 | } 53 | `} 54 | `; 55 | 56 | export const ErrorMessage = styled.span` 57 | display: block; 58 | margin-top: 5px; 59 | color: #e41111; 60 | `; 61 | 62 | export const List = styled.ul` 63 | margin-top: 30px; 64 | list-style-type: none; 65 | font-size: 16px; 66 | 67 | li { 68 | padding: 15px 0; 69 | display: flex; 70 | flex-direction: row; 71 | justify-content: space-between; 72 | align-items: center; 73 | 74 | & + li { 75 | border-top: 1px solid #eee; 76 | } 77 | 78 | img { 79 | width: 32px; 80 | margin-right: 12px; 81 | border-radius: 50%; 82 | border: 2px solid #dbdbdb; 83 | } 84 | 85 | a { 86 | display: flex; 87 | align-items: center; 88 | color: inherit; 89 | text-decoration: none; 90 | 91 | &:hover { 92 | color: #7159c1; 93 | } 94 | } 95 | 96 | button { 97 | color: #999; 98 | background: none; 99 | border: 0; 100 | padding: 6px 0 6px 16px; 101 | 102 | &:hover { 103 | color: #7159c1; 104 | } 105 | } 106 | } 107 | `; 108 | -------------------------------------------------------------------------------- /src/pages/Main/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "./Main.js" 3 | } -------------------------------------------------------------------------------- /src/pages/Repository/Repository.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import { FaStar, FaRegFileAlt, FaGithubAlt, FaSpinner } from 'react-icons/fa'; 5 | import { GoRepoForked, GoArrowLeft, GoArrowRight } from 'react-icons/go'; 6 | import api from '../../services/api'; 7 | import { 8 | Loading, 9 | Owner, 10 | IssueList, 11 | FilterList, 12 | PageNav, 13 | OwnerProfile, 14 | RepoInfo, 15 | IssueLabel, 16 | } from './RepositoryStyles'; 17 | import Container, { Icon } from '../../components/Container'; 18 | 19 | export default class Repository extends Component { 20 | static propTypes = { 21 | match: PropTypes.shape({ 22 | params: PropTypes.shape({ 23 | repo: PropTypes.string, 24 | }), 25 | }).isRequired, 26 | }; 27 | 28 | state = { 29 | repo: {}, 30 | issues: [], 31 | loading: true, 32 | filters: [ 33 | { state: 'all', label: 'All Issues', active: true }, 34 | { state: 'open', label: 'Open', active: false }, 35 | { state: 'closed', label: 'Closed', active: false }, 36 | ], 37 | filterIndex: 0, 38 | page: 1, 39 | }; 40 | 41 | async componentDidMount() { 42 | const { match } = this.props; 43 | const { filters } = this.state; 44 | 45 | const repoName = decodeURIComponent(match.params.repo); 46 | 47 | const [repo, issues] = await Promise.all([ 48 | await api.get(`/repos/${repoName}`), 49 | await api.get(`/repos/${repoName}/issues`, { 50 | params: { 51 | state: filters.find(filter => filter.active).state, 52 | per_page: 4, 53 | }, 54 | }), 55 | ]); 56 | 57 | this.setState({ 58 | repo: repo.data, 59 | issues: issues.data, 60 | loading: false, 61 | }); 62 | } 63 | 64 | loadFilters = async () => { 65 | const { match } = this.props; 66 | const { filters, filterIndex, page } = this.state; 67 | 68 | const repoName = decodeURIComponent(match.params.repo); 69 | 70 | const response = await api.get(`/repos/${repoName}/issues`, { 71 | params: { 72 | state: filters[filterIndex].state, 73 | per_page: 4, 74 | page, 75 | }, 76 | }); 77 | 78 | this.setState({ issues: response.data }); 79 | }; 80 | 81 | handleFilters = async filterIndex => { 82 | await this.setState({ filterIndex }); 83 | this.loadFilters(); 84 | }; 85 | 86 | handlePage = async action => { 87 | const { page } = this.state; 88 | await this.setState({ page: action === 'back' ? page - 1 : page + 1 }); 89 | this.loadFilters(); 90 | }; 91 | 92 | render() { 93 | const { repo, issues, loading, filters, filterIndex, page } = this.state; 94 | 95 | if (loading) { 96 | return ( 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | ); 106 | } 107 | 108 | return ( 109 | 110 | 111 | 112 | 113 | 114 |
    115 | 116 | Back to Repositories 117 | 118 |
    119 | 120 | 125 | {repo.owner.login} 126 | 127 |

    {repo.owner.login}

    128 |
    129 | 130 |

    131 | 132 | {repo.name} 133 | 134 |

    135 |
    136 | {repo.license && ( 137 | 138 | {repo.license.name} 139 | 140 | )} 141 | {repo.stargazers_count !== 0 && ( 142 | 143 | 144 | {`${Number(repo.stargazers_count).toLocaleString(undefined, { 145 | minimumIntegerDigits: 2, 146 | })} ${repo.stargazers_count === 1 ? 'star' : 'stars'}`} 147 | 148 | )} 149 | {repo.forks !== 0 && ( 150 | 151 | 152 | {`${Number(repo.forks_count).toLocaleString()} ${ 153 | repo.forks_count === 1 ? 'fork' : 'forks' 154 | }`} 155 | 156 | )} 157 |
    158 |

    {repo.description}

    159 |
    160 |
    161 | 162 | 163 | 164 | {filters.map((filter, index) => ( 165 | 172 | ))} 173 | 174 | {issues.map(issue => ( 175 |
  • 176 | 181 | {issue.user.login} 182 |
    183 | 184 | {issue.title} 185 | {issue.labels.map(label => ( 186 | 187 | {label.name} 188 | 189 | ))} 190 | 191 |

    {issue.user.login}

    192 |
    193 |
    194 |
  • 195 | ))} 196 | 197 | 205 | 209 | 210 |
    211 |
    212 | ); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/pages/Repository/RepositoryStyles.js: -------------------------------------------------------------------------------- 1 | import styled, { keyframes, css } from 'styled-components'; 2 | import colorContrast from '../../helpers/colorContrast'; 3 | 4 | const rotate = keyframes` 5 | from { 6 | transform: rotate(0deg) 7 | } 8 | 9 | to { 10 | transform: rotate(360deg) 11 | } 12 | `; 13 | 14 | export const Loading = styled.div` 15 | background: #fff; 16 | font-size: 30px; 17 | font-weight: bold; 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | height: 731px; 22 | 23 | ${props => 24 | props.loading && 25 | css` 26 | svg { 27 | font-size: 40px; 28 | animation: ${rotate} 2s linear infinite; 29 | color: #7159c1 !important; 30 | } 31 | `} 32 | `; 33 | 34 | export const Owner = styled.header` 35 | display: flex; 36 | align-items: center; 37 | justify-content: center; 38 | flex-wrap: wrap; 39 | 40 | div:first-child { 41 | align-self: flex-start; 42 | flex: 1 1 100%; 43 | margin-bottom: 40px; 44 | 45 | & > a { 46 | color: #7159c1; 47 | font-size: 16px; 48 | text-decoration: none; 49 | 50 | &:hover { 51 | color: #907dcf; 52 | } 53 | 54 | & svg { 55 | vertical-align: top; 56 | margin-right: 4px; 57 | } 58 | } 59 | } 60 | `; 61 | 62 | export const OwnerProfile = styled.div` 63 | display: flex; 64 | flex-direction: column; 65 | align-items: center; 66 | margin-right: 40px; 67 | align-self: flex-start; 68 | 69 | @media (max-width: 600px) { 70 | margin: 0 0 5px 0; 71 | } 72 | 73 | h2 { 74 | font-size: 20px; 75 | } 76 | 77 | img { 78 | width: 88px; 79 | border-radius: 50%; 80 | border: 4px solid #e6e6e6; 81 | margin-bottom: 5px; 82 | } 83 | `; 84 | 85 | export const RepoInfo = styled.div` 86 | align-self: flex-start; 87 | 88 | @media (max-width: 600px) { 89 | text-align: center; 90 | } 91 | 92 | h1 { 93 | font-size: 24px; 94 | 95 | & > a { 96 | color: inherit; 97 | text-decoration: none; 98 | 99 | &:hover { 100 | color: #7159c1; 101 | } 102 | } 103 | } 104 | 105 | & div { 106 | margin: 8px 0 16px; 107 | 108 | & span { 109 | font-size: 12px; 110 | background: #7564aa; 111 | color: #fff; 112 | padding: 4px 8px; 113 | border-radius: 3px; 114 | margin-right: 8px; 115 | 116 | & svg { 117 | vertical-align: text-top; 118 | margin-right: 4px; 119 | } 120 | } 121 | } 122 | 123 | p { 124 | font-size: 14px; 125 | color: #666; 126 | line-height: 1.4; 127 | max-width: 400px; 128 | } 129 | `; 130 | 131 | export const FilterList = styled.div` 132 | display: flex; 133 | justify-content: space-evenly; 134 | margin-bottom: 12px; 135 | border-bottom: 1px solid #eee; 136 | 137 | button { 138 | border: 0; 139 | padding: 16px 20px; 140 | margin: 0 0.5rem; 141 | background: none; 142 | color: #666; 143 | border-bottom: 2px solid transparent; 144 | text-transform: uppercase; 145 | 146 | &:nth-child(${props => props.active + 1}) { 147 | font-weight: bold; 148 | color: #7159c1; 149 | border-bottom: 2px solid #7159c1; 150 | } 151 | 152 | &:hover { 153 | color: #7159c1; 154 | } 155 | } 156 | `; 157 | 158 | export const IssueList = styled.ul` 159 | display: flex; 160 | flex-direction: column; 161 | margin-top: 30px; 162 | border-top: 1px solid #eee; 163 | list-style: none; 164 | min-height: 524px; 165 | 166 | li { 167 | & + li { 168 | margin-top: 10px; 169 | } 170 | 171 | a { 172 | padding: 15px 10px; 173 | border: 1px solid #eee; 174 | border-radius: 4px; 175 | text-decoration: none; 176 | color: #333; 177 | line-height: 21px; 178 | display: flex; 179 | transition: all 180ms ease-in-out; 180 | 181 | &:hover { 182 | color: #7159c1; 183 | border-color: #ddd; 184 | transform: scale(1.005); 185 | box-shadow: 0 12px 10px -10px hsla(254, 26%, 25%, 0.27); 186 | } 187 | } 188 | 189 | img { 190 | width: 36px; 191 | height: 36px; 192 | border-radius: 50%; 193 | border: 2px solid #eee; 194 | } 195 | 196 | div { 197 | flex: 1; 198 | margin-left: 15px; 199 | 200 | strong { 201 | font-size: 16px; 202 | 203 | & span:first-child { 204 | margin-right: 10px; 205 | } 206 | } 207 | 208 | p { 209 | margin-top: 5px; 210 | font-size: 12px; 211 | color: #999; 212 | } 213 | } 214 | } 215 | `; 216 | 217 | export const IssueLabel = styled.span` 218 | background: ${({ color }) => `#${color}`}; 219 | color: ${({ color }) => colorContrast(color)}; 220 | display: inline-block; 221 | border-radius: 2px; 222 | font-size: 12px; 223 | font-weight: 600; 224 | height: 20px; 225 | padding: 3px 8px; 226 | margin-right: 10px; 227 | line-height: 12px; 228 | `; 229 | 230 | export const PageNav = styled.div` 231 | display: flex; 232 | justify-content: space-between; 233 | padding: 15px 0 0; 234 | margin-top: auto; 235 | 236 | button { 237 | border-radius: 3px; 238 | border: 0; 239 | padding: 12px 20px; 240 | margin: 0; 241 | 242 | &:hover { 243 | background: #7159c1; 244 | color: #fff; 245 | } 246 | 247 | &[disabled] { 248 | background: rgba(0, 0, 0, 0.1); 249 | color: rgba(0, 0, 0, 0.3); 250 | cursor: auto; 251 | } 252 | 253 | svg { 254 | vertical-align: middle; 255 | font-size: 20px; 256 | } 257 | 258 | &:nth-child(1) svg { 259 | margin-right: 4px; 260 | } 261 | 262 | &:nth-child(2) svg { 263 | margin-left: 4px; 264 | } 265 | } 266 | `; 267 | -------------------------------------------------------------------------------- /src/pages/Repository/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "./Repository.js" 3 | } -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter, Route, Switch } from 'react-router-dom'; 3 | 4 | import Main from './pages/Main/Main'; 5 | import Repository from './pages/Repository/Repository'; 6 | 7 | export default function Routes() { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const api = axios.create({ 4 | baseURL: 'https://api.github.com', 5 | }); 6 | 7 | export default api; 8 | -------------------------------------------------------------------------------- /src/styles/global.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | export default createGlobalStyle` 4 | * { 5 | margin: 0; 6 | padding: 0; 7 | outline: 0; 8 | box-sizing: border-box; 9 | } 10 | 11 | html, body, #root{ 12 | min-height: 100%; 13 | } 14 | 15 | body { 16 | background: #7159c1; 17 | -webkit-font-smoothing: antialiased !important; 18 | } 19 | 20 | body, input, button { 21 | color: #333333; 22 | font-size: 14px; 23 | font-family: Arial, Helvetica, sans-serif; 24 | } 25 | 26 | button { 27 | cursor: pointer; 28 | } 29 | `; 30 | --------------------------------------------------------------------------------