├── .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 |
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 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Technologies |
33 | How To Use |
34 | License
35 |
36 |
37 | 
38 |
39 |
40 |
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 |
122 |
123 | {errorMessage && {errorMessage}}
124 |
125 |
126 | {repositories.map(repo => (
127 |
128 |
129 |
130 |

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 |
126 |
127 | {repo.owner.login}
128 |
129 |
130 |
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 |
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 |
--------------------------------------------------------------------------------