├── public
├── favicon.png
├── manifest.json
└── index.html
├── src
├── assets
│ └── luffy.jpg
├── reducers
│ ├── index.js
│ ├── issue.js
│ ├── repo.js
│ ├── repos.js
│ └── issues.js
├── containers
│ ├── index.js
│ ├── IssuePage.js
│ ├── App.js
│ ├── Issue.js
│ └── IssuesList.js
├── components
│ ├── EmptyState.js
│ ├── Accordion.js
│ ├── Select.js
│ ├── GithubCorner.js
│ ├── Animation.js
│ ├── IssueCard.js
│ └── index.js
├── styles
│ ├── accordion.css
│ └── main.css
├── store.js
├── sagas
│ ├── issue.js
│ ├── index.js
│ ├── repo.js
│ └── issues.js
├── index.js
├── firebase.js
└── requests.js
├── .eslintrc.js
├── .gitignore
├── README.md
└── package.json
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LucasdeCastro/github-careers/HEAD/public/favicon.png
--------------------------------------------------------------------------------
/src/assets/luffy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LucasdeCastro/github-careers/HEAD/src/assets/luffy.jpg
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: 'babel-eslint',
3 | extends: 'airbnb',
4 | env: {
5 | browser: true,
6 | },
7 | rules: {
8 | 'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }],
9 | 'react/prop-types': 0,
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import repo from './repo';
3 | import issue from './issue';
4 | import repos from './repos';
5 | import issues from './issues';
6 |
7 | export default combineReducers({
8 | repo,
9 | repos,
10 | issue,
11 | issues,
12 | });
13 |
--------------------------------------------------------------------------------
/src/containers/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Provider } from 'react-redux';
4 | import { HashRouter as Router } from 'react-router-dom';
5 | import App from './App';
6 | import store from '../store';
7 |
8 | export default () => (
9 |
10 |
11 |
12 |
13 |
14 | );
15 |
--------------------------------------------------------------------------------
/src/components/EmptyState.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import luffy from '../assets/luffy.jpg';
3 | import { Empty } from './index';
4 |
5 | const EmptyState = () => (
6 |
7 |
8 | Bem-vindo
9 | Selecione uma vaga na aba ao lado.
10 |
11 | );
12 |
13 | export default EmptyState;
14 |
--------------------------------------------------------------------------------
/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 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/styles/accordion.css:
--------------------------------------------------------------------------------
1 | .accordion-enter {
2 | height: 0px;
3 | opacity: 0.01;
4 | }
5 |
6 | .accordion-enter.accordion-enter-active {
7 | opacity: 1;
8 | transition: all 700ms;
9 | }
10 |
11 | .accordion-leave {
12 | opacity: 1;
13 | height: 100%;
14 | transition: 500ms;
15 | }
16 |
17 | .accordion-leave.accordion-leave-active {
18 | opacity: 0.01;
19 | height: 0px;
20 | }
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 | deploy.js*
23 | .gitconfig*
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import createSagaMiddleware from 'redux-saga';
3 | import reducers from './reducers';
4 | import { watcherSaga } from './sagas';
5 |
6 | const sagaMiddleware = createSagaMiddleware();
7 |
8 | const store = createStore(reducers, applyMiddleware(sagaMiddleware));
9 |
10 | sagaMiddleware.run(watcherSaga);
11 |
12 | export default store;
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## [GITHUB-CAREERS](http://lucasdecastro.github.io/github-careers)
2 |
3 | ### Sobre o projeto
4 |
5 | Esse projeto é uma forma de visualizar repositórios de vagas e facilitar a candidatura
6 |
7 |
8 | ### Desenvolvido com
9 |
10 | - [React](https://github.com/facebook/react)
11 |
12 | - [Redux](https://github.com/reduxjs/redux)
13 |
14 | - [Redux Saga](https://github.com/redux-saga/redux-saga)
15 |
16 | - [Styled Components](https://github.com/styled-components/styled-components)
17 |
--------------------------------------------------------------------------------
/src/sagas/issue.js:
--------------------------------------------------------------------------------
1 | import { call, put } from 'redux-saga/effects';
2 | import { getIssue } from '../requests';
3 | import {
4 | FETCH_ISSUE_FAIL,
5 | FETCH_ISSUE_SUCCESS,
6 | } from '../reducers/issue';
7 |
8 | export function* fetchIssue({ payload: { repo, id } }) {
9 | try {
10 | const { data } = yield call(getIssue, repo, id);
11 | yield put({ type: FETCH_ISSUE_SUCCESS, payload: data });
12 | } catch (errorMessage) {
13 | yield put({ type: FETCH_ISSUE_FAIL, payload: { errorMessage } });
14 | }
15 | }
16 |
17 | export default { fetchIssue };
18 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactGA from 'react-ga';
3 | import ReactDOM from 'react-dom';
4 | import { library } from '@fortawesome/fontawesome-svg-core';
5 | import { faGhost } from '@fortawesome/free-solid-svg-icons';
6 |
7 | import './styles/main.css';
8 | import App from './containers';
9 |
10 | ReactGA.initialize('UA-128061632-1');
11 |
12 | const { pathname, search, href } = window.location;
13 |
14 | ReactGA.pageview(`${pathname}/${href.split('#/')[1]}${search}`);
15 |
16 | library.add(faGhost);
17 |
18 | ReactDOM.render(, document.getElementById('root'));
19 |
--------------------------------------------------------------------------------
/src/reducers/issue.js:
--------------------------------------------------------------------------------
1 | export const FETCH_ISSUE_FAIL = 'FETCH_ISSUE_FAIL';
2 | export const FETCH_ISSUE_SUCCESS = 'FETCH_ISSUE_SUCCESS';
3 | export const FETCH_ISSUE = 'FETCH_ISSUE';
4 |
5 | export const getIssue = (repo, id) => ({ type: FETCH_ISSUE, payload: { repo, id } });
6 |
7 | export default (state = { loading: true, data: {} }, { type, payload }) => {
8 | switch (type) {
9 | case FETCH_ISSUE_SUCCESS:
10 | return { ...state, loading: false, data: payload };
11 | case FETCH_ISSUE:
12 | return { ...state, loading: true };
13 | default:
14 | return state;
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/src/sagas/index.js:
--------------------------------------------------------------------------------
1 | import { takeLatest } from 'redux-saga/effects';
2 | import { FETCH_ISSUE } from '../reducers/issue';
3 | import { FETCH_ISSUES, FETCH_ISSUES_PAGE } from '../reducers/issues';
4 | import { FETCH_REPO } from '../reducers/repo';
5 |
6 | import { fetchIssues, fetchIssuesPage } from './issues';
7 | import { fetchIssue } from './issue';
8 | import { fetchRepo } from './repo';
9 |
10 | export function* watcherSaga() {
11 | yield takeLatest(FETCH_REPO, fetchRepo);
12 | yield takeLatest(FETCH_ISSUE, fetchIssue);
13 | yield takeLatest(FETCH_ISSUES, fetchIssues);
14 | yield takeLatest(FETCH_ISSUES_PAGE, fetchIssuesPage);
15 | }
16 |
17 | export default { watcherSaga };
18 |
--------------------------------------------------------------------------------
/src/components/Accordion.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import '../styles/accordion.css';
5 | import Animation from './Animation';
6 |
7 | const Accordion = ({ children, id }) => {
8 | const [show, toggle] = useState(false);
9 | const [header, body] = children;
10 |
11 | return (
12 |
13 | {React.cloneElement(header, { click: toggle })}
14 |
15 | {show && body}
16 |
17 |
18 | );
19 | };
20 |
21 | Accordion.propTypes = {
22 | children: PropTypes.arrayOf(PropTypes.func).isRequired,
23 | id: PropTypes.number.isRequired,
24 | };
25 |
26 | export default Accordion;
27 |
--------------------------------------------------------------------------------
/src/styles/main.css:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | #root {
4 | margin: 0px;
5 | padding: 0px;
6 | font-family: "Roboto", sans-serif;
7 | background-color: #fafbfc;
8 |
9 | display: flex;
10 | flex-direction: column;
11 | }
12 |
13 | ::-webkit-scrollbar {
14 | width: 5px;
15 | z-index: 2;
16 | }
17 |
18 | /* Track */
19 | ::-webkit-scrollbar-track {
20 | background: #f1f1f1;
21 | z-index: 2;
22 | }
23 |
24 | /* Handle */
25 | ::-webkit-scrollbar-thumb {
26 | background: #e1e4e8;
27 | border-radius: 5px;
28 | }
29 |
30 | /* Handle on hover */
31 | ::-webkit-scrollbar-thumb:hover {
32 | background: #e1e4e9;
33 | }
34 |
35 | @keyframes donut-spin {
36 | 0% {
37 | transform: rotate(0deg);
38 | }
39 | 100% {
40 | transform: rotate(360deg);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/sagas/repo.js:
--------------------------------------------------------------------------------
1 | import { call, put, all } from 'redux-saga/effects';
2 | import { getRepo, getLabels } from '../requests';
3 | import { FETCH_REPO_FAIL, FETCH_REPO_SUCCESS } from '../reducers/repo';
4 |
5 | export function* fetchRepo({ repos }) {
6 | try {
7 | const labels = yield all(repos.map((e) => call(getLabels, e)));
8 | const repo = yield all(repos.map((e) => call(getRepo, e)));
9 |
10 | yield put({
11 | type: FETCH_REPO_SUCCESS,
12 | payload: {
13 | repo: [].concat(...repo.map(({ data }) => data)),
14 | labels: [].concat(...labels.map(({ data }) => data)),
15 | },
16 | });
17 | } catch (errorMessage) {
18 | yield put({ type: FETCH_REPO_FAIL, payload: { errorMessage } });
19 | }
20 | }
21 |
22 | export default fetchRepo;
23 |
--------------------------------------------------------------------------------
/src/components/Select.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Select } from './index';
4 |
5 | const SelectWrapper = ({
6 | data, getValue, getLabel, onChange,
7 | }) => {
8 | if (!data || data.length === 0) return null;
9 |
10 | return (
11 |
22 | );
23 | };
24 |
25 | SelectWrapper.propTypes = {
26 | data: PropTypes.arrayOf(PropTypes.string).isRequired,
27 | getValue: PropTypes.func.isRequired,
28 | getLabel: PropTypes.func.isRequired,
29 | onChange: PropTypes.func.isRequired,
30 | };
31 |
32 | export default SelectWrapper;
33 |
--------------------------------------------------------------------------------
/src/containers/IssuePage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import Issue from './Issue';
4 | import { getIssue } from '../reducers/issue';
5 | import {
6 | Loading,
7 | IssueComponent,
8 | } from '../components';
9 |
10 | class IssuePage extends React.Component {
11 | componentDidMount() {
12 | const {
13 | getIssueConnect,
14 | match: {
15 | params: { id, repo },
16 | },
17 | } = this.props;
18 | getIssueConnect(repo, id);
19 | }
20 |
21 | render() {
22 | const {
23 | issue: { loading, data: issue },
24 | } = this.props;
25 |
26 | if (loading) return ;
27 |
28 | return (
29 |
30 |
31 |
32 | );
33 | }
34 | }
35 |
36 | export default connect(
37 | ({ issue }) => ({ issue }),
38 | { getIssueConnect: getIssue },
39 | )(IssuePage);
40 |
--------------------------------------------------------------------------------
/src/reducers/repo.js:
--------------------------------------------------------------------------------
1 | export const SET_LABEL = 'SET_LABEL';
2 | export const FETCH_REPO = 'FETCH_REPO';
3 | export const FETCH_REPO_FAIL = 'FETCH_REPO_FAIL';
4 | export const FETCH_REPO_SUCCESS = 'FETCH_REPO_SUCCESS';
5 |
6 | const initialState = {
7 | repo: {},
8 | labels: [],
9 | error: false,
10 | loading: false,
11 | errorMessage: '',
12 | filterLabel: null,
13 | };
14 |
15 | export default function (state = initialState, { type, payload }) {
16 | switch (type) {
17 | case SET_LABEL:
18 | return {
19 | ...state,
20 | filterLabel: payload ? parseInt(payload, 10) : payload,
21 | };
22 | case FETCH_REPO:
23 | return {
24 | ...state,
25 | loading: true,
26 | error: false,
27 | errorMessage: '',
28 | };
29 | case FETCH_REPO_SUCCESS:
30 | return { ...state, loading: false, ...payload };
31 | case FETCH_REPO_FAIL:
32 | return { ...state, error: true, errorMessage: payload };
33 | default:
34 | return state;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/firebase.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase/app';
2 | import 'firebase/auth';
3 |
4 | const config = {
5 | apiKey: 'AIzaSyCpzGCkGcsRpTTCunnXb-iQ8zsldzDSg-c',
6 | authDomain: 'github-careers.firebaseapp.com',
7 | databaseURL: 'https://github-careers.firebaseio.com',
8 | projectId: 'github-careers',
9 | storageBucket: 'github-careers.appspot.com',
10 | messagingSenderId: '6118754197',
11 | };
12 |
13 | firebase.initializeApp(config);
14 |
15 | const createGithubProvider = () => {
16 | const provider = new firebase.auth.GithubAuthProvider();
17 | provider.setCustomParameters({
18 | allow_signup: 'false',
19 | });
20 |
21 | return provider;
22 | };
23 |
24 | export const githubLogin = () => {
25 | const provider = createGithubProvider();
26 |
27 | return firebase
28 | .auth()
29 | .signInWithPopup(provider)
30 | .then((result) => {
31 | const token = result.credential.accessToken;
32 | localStorage.setItem('access_token', token);
33 | return result;
34 | });
35 | };
36 |
37 | export default firebase;
38 |
--------------------------------------------------------------------------------
/src/reducers/repos.js:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | list: ['frontendbr', 'backend-br', 'react-brasil'],
3 | filter: [],
4 | };
5 |
6 | export const TYPES = {
7 | ADD_REPO: 'ADD_REPO',
8 | FILTER_REPO: 'FILTER_REPO',
9 | REMOVE_FILTER: 'REMOVE_FILTER',
10 | };
11 |
12 | export const addRepo = (payload) => ({ type: TYPES.ADD_REPO, payload });
13 | export const removeFilter = (payload) => ({ type: TYPES.REMOVE_FILTER, payload });
14 | export const filterRepo = (payload) => ({ type: TYPES.FILTER_REPO, payload });
15 |
16 | const repos = (state = initialState, { payload, type }) => {
17 | switch (type) {
18 | case TYPES.ADD_REPO:
19 | return {
20 | ...state,
21 | list: state.list.concat(payload),
22 | };
23 | case TYPES.FILTER_REPO:
24 | return {
25 | ...state,
26 | filter: state.filter.concat(payload),
27 | };
28 | case TYPES.REMOVE_FILTER:
29 | return {
30 | ...state,
31 | filter: state.filter.filter((repo) => repo !== payload),
32 | };
33 | default:
34 | return state;
35 | }
36 | };
37 |
38 | export default repos;
39 |
--------------------------------------------------------------------------------
/src/requests.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const createAxiosInstance = () => {
4 | const instance = axios.create({
5 | baseURL: BASE_URL,
6 | });
7 | instance.interceptors.request.use((config) => {
8 | const token = localStorage.getItem('access_token');
9 |
10 | if (token !== null) {
11 | config.headers.Authorization = token;
12 | }
13 |
14 | return config;
15 | }, function (err) {
16 | return Promise.reject(err);
17 | });
18 | return instance;
19 | };
20 |
21 | export const BASE_URL = 'https://api.github.com/repos/';
22 | export const instance = createAxiosInstance();
23 |
24 | export function getIssue(repo, id) {
25 | return instance.get(`${repo}/vagas/issues/${id}`);
26 | }
27 |
28 | export function getIssues(repo) {
29 | return instance.get(`${repo}/vagas/issues`);
30 | }
31 |
32 | export function getIssuesPage(repo, page) {
33 | return instance.get(`${repo}/vagas/issues?page=${page}`);
34 | }
35 |
36 | export function getRepo(repo) {
37 | return instance.get(`${repo}/vagas`);
38 | }
39 | export function getLabels(repo) {
40 | return instance.get(`${repo}/vagas/labels`);
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/GithubCorner.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const GithubCorner = ({ repo, title }) => {
5 | const styles = {
6 | position: 'absolute',
7 | top: '0',
8 | right: '0',
9 | };
10 |
11 | return (
12 |
13 |
18 |
19 | );
20 | };
21 |
22 | GithubCorner.propTypes = {
23 | repo: PropTypes.string,
24 | title: PropTypes.string,
25 | };
26 |
27 | GithubCorner.defaultProps = {
28 | repo: 'https://github.com/LucasdeCastro/github-careers',
29 | title: 'Github',
30 | };
31 |
32 | export default GithubCorner;
33 |
--------------------------------------------------------------------------------
/src/sagas/issues.js:
--------------------------------------------------------------------------------
1 | import { call, put, all } from 'redux-saga/effects';
2 | import { getIssues, getIssuesPage } from '../requests';
3 | import {
4 | FETCH_ISSUES_FAIL,
5 | FETCH_ISSUES_SUCCESS,
6 | FETCH_ISSUES_PAGE_SUCCESS,
7 | } from '../reducers/issues';
8 |
9 | export function* fetchIssues({ payload: repos }) {
10 | try {
11 | const data = yield all(repos.map((repo) => call(getIssues, repo)));
12 |
13 | const sorted = []
14 | .concat(...data.map((response) => response.data))
15 | .sort((a, b) => {
16 | if (a.created_at < b.created_at) return 1;
17 | if (a.created_at === b.created_at) return 0;
18 | return -1;
19 | });
20 | yield put({ type: FETCH_ISSUES_SUCCESS, payload: { data: sorted } });
21 | } catch (errorMessage) {
22 | yield put({ type: FETCH_ISSUES_FAIL, payload: { errorMessage } });
23 | }
24 | }
25 |
26 | export function* fetchIssuesPage({ page, repos }) {
27 | try {
28 | const data = yield all(repos.map((repo) => call(getIssuesPage, repo, page)));
29 | const sorted = []
30 | .concat(...data.map((response) => response.data))
31 | .sort((a, b) => {
32 | if (a.created_at < b.created_at) return 1;
33 | if (a.created_at === b.created_at) return 0;
34 | return -1;
35 | });
36 |
37 | yield put({ type: FETCH_ISSUES_PAGE_SUCCESS, payload: { data: sorted } });
38 | } catch (errorMessage) {
39 | yield put({ type: FETCH_ISSUES_FAIL, payload: { errorMessage } });
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
14 |
15 |
16 |
20 |
29 | Github careers
30 |
31 |
32 |
33 |
34 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/components/Animation.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 |
5 | export default class Animation extends React.Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.state = {
10 | key: `key-${Date.now()}`,
11 | };
12 | }
13 |
14 | enter() {
15 | if (this.refs[this.state.key]) {
16 | const { name } = this.props;
17 | const node = ReactDOM.findDOMNode(this.refs[this.state.key]);
18 |
19 | node.classList.remove(`${name}-leave`);
20 | node.classList.remove(`${name}-leave-active`);
21 |
22 | node.classList.add(`${name}-enter`);
23 |
24 | setTimeout((_) => {
25 | const height = Array.prototype.reduce.call(
26 | node.children,
27 | (acc, x) => acc + x.clientHeight,
28 | 0,
29 | );
30 |
31 | node.style.height = `${height}px`;
32 | node.classList.add(`${name}-enter-active`);
33 | }, 0);
34 | }
35 | }
36 |
37 | leave() {
38 | if (this.refs[this.state.key]) {
39 | const { name, leaveTime } = this.props;
40 | const node = ReactDOM.findDOMNode(this.refs[this.state.key]);
41 |
42 | node.classList.remove(`${name}-enter`);
43 | node.classList.remove(`${name}-enter-active`);
44 |
45 | node.classList.add(`${name}-leave`);
46 |
47 | setTimeout((_) => {
48 | this.children = null;
49 | node.style.height = '0px';
50 | node.classList.add(`${name}-leave-active`);
51 |
52 | setTimeout((_) => this.forceUpdate(), leaveTime);
53 | }, 0);
54 | }
55 | }
56 |
57 | componentWillUpdate(nextProps) {
58 | if (nextProps.children) {
59 | this.children = nextProps.children;
60 | this.enter();
61 | } else if (this.children) this.leave();
62 | }
63 |
64 | render() {
65 | const { key } = this.state;
66 | return {this.children}
;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "git-careers",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@fortawesome/fontawesome-svg-core": "^1.2.25",
7 | "@fortawesome/free-solid-svg-icons": "^5.11.2",
8 | "@fortawesome/react-fontawesome": "^0.1.5",
9 | "axios": "^0.19.0",
10 | "firebase": "^7.1.0",
11 | "marked": "^0.7.0",
12 | "prop-types": "^15.7.2",
13 | "react": "^16.10.2",
14 | "react-dom": "^16.10.2",
15 | "react-ga": "^2.6.0",
16 | "react-icons": "^2.2.7",
17 | "react-markdown": "^4.2.2",
18 | "react-redux": "^5.1.1",
19 | "react-router-dom": "^5.1.2",
20 | "react-scripts": "^3.2.0",
21 | "redux": "^4.0.4",
22 | "redux-saga": "^1.1.1",
23 | "styled-components": "^4.4.0"
24 | },
25 | "scripts": {
26 | "predeploy": "react-scripts build",
27 | "deploy": "gh-pages -d build",
28 | "deploy:user": "node deploy",
29 | "start": "react-scripts start",
30 | "build": "react-scripts build",
31 | "test": "react-scripts test --env=jsdom",
32 | "eject": "react-scripts eject",
33 | "lint": "eslint src/**/*.js --fix"
34 | },
35 | "husky": {
36 | "hooks": {
37 | "pre-push": "npm run lint",
38 | "pre-commit": "npm run lint"
39 | }
40 | },
41 | "devDependencies": {
42 | "babel-eslint": "^10.0.3",
43 | "eslint": "^6.5.1",
44 | "eslint-config-airbnb": "^18.0.1",
45 | "eslint-plugin-babel": "^5.3.0",
46 | "eslint-plugin-import": "^2.18.2",
47 | "eslint-plugin-jsx-a11y": "^6.2.3",
48 | "eslint-plugin-react": "^7.15.1",
49 | "eslint-plugin-react-hooks": "^1.7.0",
50 | "gh-pages": "^1.1.0",
51 | "husky": "^3.0.8"
52 | },
53 | "homepage": "https://lucasdecastro.github.io/github-careers",
54 | "browserslist": {
55 | "production": [
56 | ">0.2%",
57 | "not dead",
58 | "not op_mini all"
59 | ],
60 | "development": [
61 | "last 1 chrome version",
62 | "last 1 firefox version",
63 | "last 1 safari version"
64 | ]
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/containers/App.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import React, { Component } from 'react';
3 | import PropTypes from 'prop-types';
4 | import { Route } from 'react-router-dom';
5 | import { githubLogin } from '../firebase';
6 | import {
7 | Main,
8 | Title,
9 | Header,
10 | Container,
11 | HeaderContainer,
12 | LoginButton,
13 | } from '../components';
14 | import IssuePage from './IssuePage';
15 | import Select from '../components/Select';
16 | import IssuesList from './IssuesList';
17 | import { FETCH_REPO, SET_LABEL } from '../reducers/repo';
18 |
19 | class App extends Component {
20 | constructor() {
21 | super();
22 | this.state = {
23 | isLogged: localStorage.getItem('access_token'),
24 | };
25 | }
26 |
27 | componentDidMount() {
28 | const { fetchRepo, repos } = this.props;
29 | fetchRepo(repos.list);
30 | }
31 |
32 | setLabel = (el) => {
33 | const { setLabel } = this.props;
34 | el.target.blur();
35 | const value = el.target.value || null;
36 | setLabel(value);
37 | };
38 |
39 | login = () => {
40 | githubLogin().then((user) => {
41 | const token = user.credential.accessToken;
42 | this.setState({ isLogged: token });
43 | });
44 | };
45 |
46 | render() {
47 | const {
48 | repo: { labels },
49 | } = this.props;
50 | const { isLogged } = this.state;
51 | return (
52 |
53 |
54 |
55 | github careers
56 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | );
74 | }
75 | }
76 |
77 | App.propTypes = {
78 | setLabel: PropTypes.func.isRequired,
79 | fetchRepo: PropTypes.func.isRequired,
80 | repos: PropTypes.objectOf({
81 | list: PropTypes.arrayOf,
82 | }).isRequired,
83 | repo: PropTypes.shape({
84 | labels: PropTypes.arrayOf(PropTypes.string),
85 | }).isRequired,
86 | };
87 |
88 | const mapDispatch = (dispatch) => ({
89 | fetchRepo: (repos) => dispatch({ type: FETCH_REPO, repos }),
90 | setLabel: (id) => dispatch({ type: SET_LABEL, payload: id }),
91 | });
92 |
93 | export default connect(
94 | ({ repo, repos }) => ({ repo, repos }),
95 | mapDispatch,
96 | )(App);
97 |
--------------------------------------------------------------------------------
/src/reducers/issues.js:
--------------------------------------------------------------------------------
1 | import { SET_LABEL } from './repo';
2 |
3 | export const FETCH_ISSUES = 'FETCH_ISSUES';
4 | export const FILTER_TITLE = 'FILTER_TITLE';
5 | export const FETCH_ISSUES_PAGE = 'FETCH_ISSUES_PAGE';
6 | export const FETCH_ISSUES_FAIL = 'FETCH_ISSUES_FAIL';
7 | export const FETCH_ISSUES_SUCCESS = 'FETCH_ISSUES_SUCCESS';
8 | export const FETCH_ISSUES_PAGE_SUCCESS = 'FETCH_ISSUES_PAGE_SUCCESS';
9 |
10 | const initialState = {
11 | page: 1,
12 | data: [],
13 | filterTitle: '',
14 | filterLabel: '',
15 | filterData: [],
16 | error: false,
17 | loading: false,
18 | errorMessage: '',
19 | };
20 |
21 | const filterByLabel = (payload, { data, filterTitle }) => {
22 | if (!payload) return [];
23 |
24 | const filtered = data.filter(
25 | (issue) => issue.labels.length
26 | && issue.labels.find(({ id }) => id === parseInt(payload, 10)),
27 | );
28 |
29 | return filterTitle
30 | // eslint-disable-next-line
31 | ? filterByTitle(filterTitle, { data: filtered })
32 | : filtered;
33 | };
34 |
35 | const filterByTitle = (payload, { data, filterLabel }) => {
36 | const filtered = data.filter(
37 | (issue) => issue.title
38 | && payload
39 | && issue.title.toLowerCase().indexOf(payload.toLowerCase()) >= 0,
40 | );
41 | return filterLabel
42 | ? filterByLabel(filterLabel, { data: filtered })
43 | : filtered;
44 | };
45 |
46 | const nextPage = (state, payload) => {
47 | const newData = state.data.concat(payload.data);
48 | const filtered = filterByTitle(state.filterTitle, {
49 | ...state,
50 | data: newData,
51 | });
52 | return {
53 | ...state,
54 | page: state.page + 1,
55 | loading: false,
56 | data: newData,
57 | filterData: filtered,
58 | };
59 | };
60 |
61 | export default function (state = initialState, { type, payload }) {
62 | switch (type) {
63 | case FETCH_ISSUES:
64 | case FETCH_ISSUES_PAGE:
65 | return { ...state, loading: true };
66 | case FILTER_TITLE:
67 | return {
68 | ...state,
69 | filterData: filterByTitle(payload, state),
70 | filterTitle: payload,
71 | };
72 | case SET_LABEL:
73 | return {
74 | ...state,
75 | filterData: filterByLabel(payload, state),
76 | filterLabel: payload,
77 | };
78 | case FETCH_ISSUES_SUCCESS:
79 | return { ...state, data: payload.data, loading: false };
80 | case FETCH_ISSUES_PAGE_SUCCESS:
81 | return nextPage(state, payload);
82 | case FETCH_ISSUES_FAIL:
83 | return {
84 | ...state,
85 | error: true,
86 | loading: false,
87 | errorMessage: payload.message || payload.errorMessage,
88 | };
89 | default:
90 | return state;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/IssueCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {
4 | Row,
5 | Card,
6 | Bold,
7 | GitLabel,
8 | LabelRow,
9 | CardTitle,
10 | TitleDate,
11 | TitleContainer,
12 | } from './index';
13 |
14 | const openJobPage = (item, repos) => () => {
15 | const baseUrl = window.location.href;
16 | const repo = repos.filter((repokey) => item.url.includes(repokey));
17 | window.open(`${baseUrl}${repo}/${item.number}`, '_blank');
18 | };
19 |
20 | const handleClick = (item, repos, click) => (event) => {
21 | if (event.ctrlKey || event.metaKey) {
22 | openJobPage(item, repos);
23 | return;
24 | }
25 |
26 | click(item, repos);
27 | };
28 |
29 | const IssueCard = ({
30 | click, repos, item, selected,
31 | }) => {
32 | const { labels, title, created_at: createdAt } = item;
33 | const date = new Date(createdAt);
34 | const month = (date.getMonth() + 1).toString().padStart(2, 0);
35 |
36 | const day = date
37 | .getDate()
38 | .toString()
39 | .padStart(2, 0);
40 |
41 | const dateStr = `${day}/${month}/${date.getFullYear()}`;
42 | const titleSplited = (title || '').split(/\[(.*?)\]/g);
43 |
44 | return (
45 |
46 |
47 |
48 | {titleSplited.length <= 3 ? (
49 | <>
50 | {titleSplited[2] && titleSplited[2]}
51 | {titleSplited[1] && (
52 |
53 |
54 | {`[${titleSplited[1]}]`}
55 |
56 | {dateStr}
57 |
58 | )}
59 | >
60 | ) : title}
61 | {titleSplited.length === 1 && titleSplited[0]}
62 |
63 |
64 |
65 |
66 |
67 | {(labels || []).slice(0, 3).map(({ color, name, id }) => (
68 |
69 | {name}
70 |
71 | ))}
72 |
73 |
74 |
75 | );
76 | };
77 |
78 | IssueCard.propTypes = {
79 | click: PropTypes.func,
80 | item: PropTypes.shape({
81 | title: PropTypes.string.isRequired,
82 | created_at: PropTypes.string.isRequired,
83 | labels: PropTypes.arrayOf(PropTypes.string),
84 | }).isRequired,
85 | selected: PropTypes.number.isRequired,
86 | repos: PropTypes.arrayOf(PropTypes.string).isRequired,
87 | };
88 |
89 | IssueCard.defaultProps = {
90 | click: () => null,
91 | };
92 |
93 | export default IssueCard;
94 |
--------------------------------------------------------------------------------
/src/containers/Issue.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-danger */
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import marked from 'marked';
5 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
6 | import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
7 | import {
8 | MarkdownContainer, ApplyButton, MarkdownTitle, ButtonAlt,
9 | } from '../components';
10 |
11 | marked.setOptions({
12 | renderer: new marked.Renderer(),
13 | pedantic: false,
14 | gfm: true,
15 | breaks: false,
16 | sanitize: false,
17 | smartLists: true,
18 | smartypants: false,
19 | xhtml: false,
20 | });
21 |
22 |
23 | const EMAIL_URL = 'https://mail.google.com/mail/u/0/?view=cm&fs=1&tf=1';
24 |
25 | export const getURL = (item, repos) => {
26 | const baseUrl = window.location.href;
27 | const repo = repos.filter((repokey) => item.url.includes(repokey));
28 | return `${baseUrl}${repo}/${item.number}`;
29 | };
30 |
31 | export const openJobPage = (item, repos) => () => {
32 | window.open(getURL(item, repos), '_blank');
33 | };
34 |
35 | class Issue extends React.PureComponent {
36 | getSubject = (body) => {
37 | const str = body.match(/(?=(assunto|subject)).*$/gim);
38 | if (str) {
39 | return str[0]
40 | .replace(/(assunto|subject)/g, '')
41 | .split('.')[0]
42 | .replace(/[^a-zA-Z ]/g, '')
43 | .trim();
44 | }
45 |
46 | return undefined;
47 | };
48 |
49 | getEmails = (body) => {
50 | const emails = body.match(
51 | /([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)/gi,
52 | );
53 |
54 | return (
55 | emails
56 | && emails.reduce((xs, _x) => {
57 | const x = _x.trim();
58 | if (xs.indexOf(x) < 0) xs.push(x);
59 | return xs;
60 | }, [])
61 | );
62 | };
63 |
64 | openEmail = (emails, subject = '') => {
65 | const reg = /Android|iPhone|iPad|iPod/i;
66 | if (reg.test(navigator.userAgent)) {
67 | return window.open(`mailto:${emails.join(',')}?&subject=${subject}`);
68 | }
69 | return window.open(`${EMAIL_URL}&to=${emails.join(',')}&su=${subject}`);
70 | };
71 |
72 | sendEmail = () => {
73 | const { item } = this.props;
74 | if (!item) return;
75 |
76 | const subject = this.getSubject(item.body);
77 | const emails = this.getEmails(item.body);
78 |
79 | // eslint-disable-next-line
80 | if (!emails) return alert('Verifique na descrição como aplicar para essa vaga');
81 |
82 | this.openEmail(emails, subject);
83 | };
84 |
85 | render() {
86 | const {
87 | item, history, repos, noFixed, isPage = false,
88 | } = this.props;
89 | const body = item.body.split('-->');
90 | const parsed = marked(body[1] || body[0]);
91 | if (!item) return history.replace({ pathname: '/' });
92 |
93 | return (
94 |
95 |
96 | {item.title}
97 | Candidatar-se agora
98 | {!isPage && (
99 |
100 |
101 |
102 | )}
103 |
104 |
105 |
108 |
109 | );
110 | }
111 | }
112 |
113 |
114 | Issue.propTypes = {
115 | isPage: PropTypes.bool.isRequired,
116 | noFixed: PropTypes.bool.isRequired,
117 | item: PropTypes.objectOf({ body: PropTypes.string }).isRequired,
118 | repos: PropTypes.arrayOf(PropTypes.string).isRequired,
119 | history: PropTypes.func.isRequired,
120 | };
121 |
122 | export default Issue;
123 |
--------------------------------------------------------------------------------
/src/containers/IssuesList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import {
4 | Row,
5 | RepoTab,
6 | Message,
7 | Loading,
8 | InputSearch,
9 | TabContainer,
10 | ScrollContainer,
11 | IssueListContainer,
12 | } from '../components';
13 |
14 | import Issue from './Issue';
15 | import IssueCard from '../components/IssueCard';
16 | import EmptyState from '../components/EmptyState';
17 |
18 | import {
19 | FETCH_ISSUES,
20 | FETCH_ISSUES_PAGE,
21 | FILTER_TITLE,
22 | } from '../reducers/issues';
23 | import { filterRepo, removeFilter } from '../reducers/repos';
24 |
25 | class IssuesList extends Component {
26 | constructor(props) {
27 | super(props);
28 |
29 | this.state = { selectedItem: null, isMobile: document.body.offsetWidth <= 1200 };
30 | }
31 |
32 | componentDidMount() {
33 | const {
34 | fetchIssues,
35 | repos: { list, filter },
36 | } = this.props;
37 | fetchIssues(list.filter((repo) => !filter.includes(repo)));
38 | }
39 |
40 | componentDidUpdate() {
41 | const { selectedItem } = this.state;
42 | const { issues: { filterData } } = this.props;
43 | if (!selectedItem && filterData.length) {
44 | setTimeout(() => this.setState({ selectedItem: filterData[0] }), 0);
45 | }
46 | }
47 |
48 | onScroll = (el) => {
49 | const scroll = el.target;
50 | const currentPosition = scroll.clientHeight + scroll.scrollTop;
51 | const aroundEnd = scroll.scrollHeight - 240 <= currentPosition;
52 | if (aroundEnd) this.getNextPage();
53 | };
54 |
55 | getNextPage = () => {
56 | const {
57 | fetchNextPage,
58 | issues: { loading, page },
59 | repos: { list, filter },
60 | } = this.props;
61 | if (loading) return;
62 |
63 | fetchNextPage(
64 | page + 1,
65 | list.filter((repo) => !filter.includes(repo)),
66 | );
67 | };
68 |
69 | onSearch = (el) => {
70 | const { filterTitle } = this.props;
71 | const { value } = el.target;
72 | filterTitle(value);
73 | };
74 |
75 | toggleFilter = (repo, filtered) => () => {
76 | const {
77 | repos: { list, filter },
78 | fetchIssues,
79 | filterRepoConnect,
80 | removeFilterConnect,
81 | } = this.props;
82 |
83 | if (filtered) {
84 | fetchIssues(
85 | list.filter((item) => item === repo || !filter.includes(item)),
86 | );
87 | removeFilterConnect(repo);
88 | } else {
89 | fetchIssues(
90 | list.filter((item) => item !== repo && !filter.includes(item)),
91 | );
92 | filterRepoConnect(repo);
93 | }
94 | };
95 |
96 | selectItem = (selectedItem) => {
97 | const { repos: { list: repos }, history } = this.props;
98 | const { isMobile } = this.state;
99 | if (isMobile) {
100 | const repo = repos.filter((repokey) => selectedItem.url.includes(repokey));
101 | history.push(`${repo}/${selectedItem.number}`);
102 | } else {
103 | this.setState({ selectedItem });
104 | }
105 | }
106 |
107 | renderList() {
108 | const {
109 | issues: { data, filterData },
110 | repos,
111 | } = this.props;
112 | const { selectedItem = {} } = this.state;
113 | const list = filterData.length ? filterData : data;
114 |
115 | return list.map((el) => (
116 |
117 | ));
118 | }
119 |
120 | render() {
121 | const {
122 | repos: { list, filter },
123 | issues: { loading, error },
124 | history,
125 | } = this.props;
126 | const { selectedItem, isMobile } = this.state;
127 | if (error) {
128 | return (
129 |
130 | Você atingiu o limite de requisições sem esta logado, realize o login
131 | para continuar ou aguarde 40 minutos
132 |
133 | );
134 | }
135 |
136 | return (
137 |
138 |
139 |
140 | {list.map((repo) => (
141 |
146 | {repo}
147 |
148 | ))}
149 |
150 |
151 |
152 | {this.renderList()}
153 |
154 |
155 |
156 | {!isMobile && (
157 |
158 | {selectedItem && }
159 | {!selectedItem && (
160 |
161 | )}
162 |
163 | )}
164 |
165 |
166 | );
167 | }
168 | }
169 |
170 | const mapDispatch = (dispatch) => ({
171 | fetchIssues: (repos) => dispatch({ type: FETCH_ISSUES, payload: repos }),
172 | filterTitle: (filter) => dispatch({ type: FILTER_TITLE, payload: filter }),
173 | fetchNextPage: (page, repos) => dispatch({ type: FETCH_ISSUES_PAGE, page, repos }),
174 | filterRepoConnect: (repo) => dispatch(filterRepo(repo)),
175 | removeFilterConnect: (repo) => dispatch(removeFilter(repo)),
176 | });
177 |
178 | export default connect(
179 | ({ issues, repo: { filterLabel }, repos }) => ({
180 | issues,
181 | filterLabel,
182 | repos,
183 | }),
184 | mapDispatch,
185 | )(IssuesList);
186 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Main = styled.div`
4 | width: 1200px;
5 | height: 100%;
6 | margin: 45px auto;
7 |
8 | @media only screen and (max-width: 1200px) {
9 | width: 100%;
10 | }
11 | `;
12 |
13 | export const Container = styled.div`
14 | display: flex;
15 | flex-direction: column;
16 | `;
17 |
18 | export const Row = styled.div`
19 | display: flex;
20 | flex-direction: row;
21 | `;
22 |
23 | export const InputSearch = styled.input`
24 | flex: 1;
25 | border: 1px solid #ccc;
26 | padding: 10px 12px;
27 | border-radius: 3px;
28 | margin: 20px 0px;
29 | `;
30 |
31 | export const Header = styled.div`
32 | background: #24292e;
33 | color: rgba(255, 255, 255, 0.75);
34 | box-sizing: border-box;
35 |
36 | top: 0px;
37 | left: 0px;
38 | width: 100%;
39 | position: fixed;
40 | `;
41 |
42 | export const HeaderContainer = styled.div`
43 | width: 1200px;
44 | min-height: 50px;
45 | display: flex;
46 | margin: 0 auto;
47 | align-items: center;
48 |
49 | @media only screen and (max-width: 1200px) {
50 | width: 100%;
51 | }
52 | `;
53 |
54 | export const CardTitle = styled.label`
55 | flex: 1;
56 | color: #585858;
57 | cursor: pointer;
58 | font-size: 14px;
59 | font-weight: 700;
60 | line-height: 27px;
61 | text-decoration: none;
62 | box-sizing: border-box;
63 | text-transform: uppercase;
64 |
65 | :hover {
66 | color: #3CA2E0;
67 | }
68 |
69 | @media only screen and (max-width: 900px) {
70 | font-size: 15px;
71 | }
72 | `;
73 |
74 | export const Card = styled.div`
75 | display: flex;
76 | cursor: pointer;
77 | min-height: 80px;
78 | box-sizing: border-box;
79 | flex-direction: column;
80 | background-color: #fff;
81 | padding: 15px 15px 5px 15px;
82 | border-bottom: 1px solid #d1d5da;
83 |
84 | ${(props) => (props.selected ? 'border: 2px solid #3CA2E0' : '')}
85 | ${(props) => (props.selected ? 'background: #f1f2f2' : '')}
86 | `;
87 |
88 | export const LabelRow = styled.div`
89 | flex: 1;
90 | display: flex;
91 | flex-wrap: wrap;
92 | margin: 5px 0px 10px 0px;
93 | `;
94 |
95 | export const GitLabel = styled.div`
96 | height: 15px;
97 | font-size: 12px;
98 | font-weight: 600;
99 | margin-right: 5px;
100 | line-height: 15px;
101 | border-bottom: 3px solid #${(props) => props.color};
102 | `;
103 |
104 | export const TitleContainer = styled.div`
105 | display: flex;
106 | `;
107 |
108 | export const TitleDate = styled.label`
109 | color: #ccc;
110 | line-height: 27px;
111 | padding-left: 5px;
112 | `;
113 |
114 | export const MarkdownContainer = styled.div`
115 | display: flex;
116 | padding: 10px;
117 | border-bottom: none;
118 | flex-direction: column;
119 | border: 1px solid #e1e4e8;
120 | border-top: 0px;
121 | background: #fff;
122 | z-index: -1;
123 | color: #404040;
124 |
125 | img {
126 | width: 100%;
127 | }
128 | `;
129 |
130 | export const MarkdownTitle = styled.div`
131 | color: #333;
132 | font-weight: 600;
133 | padding: 10px;
134 | border-bottom: 1px solid #d1d5da;
135 | background: #FFF;
136 | margin: -10px;
137 |
138 | ${({ noFixed }) => (!noFixed ? 'position: absolute;' : '')}
139 | ${({ noFixed }) => (noFixed ? 'border-top: 1px solid #d1d5da;' : '')}
140 | ${({ noFixed }) => (noFixed ? 'width: 100%' : 'width: 750px;')}
141 |
142 | h2 {
143 | margin: 0px;
144 | flex: 1 1 0%;
145 | padding: 10px;
146 | display: flex;
147 | font-size: 20px;
148 | align-items: center;
149 | }
150 | `;
151 |
152 | export const ApplyButton = styled.button`
153 | color: #fff;
154 | margin: 10px 0px;
155 | border: none;
156 | width: 200px;
157 | height: 40px;
158 | padding: 0 16px;
159 | cursor: pointer;
160 | border-radius: 2px;
161 | text-align: center;
162 | background: #1861bf;
163 | align-self: flex-start;
164 | font-weight: 500;
165 | font-size: 15px;
166 |
167 | `;
168 |
169 | export const ButtonAlt = styled.button`
170 | border: 1px solid #1861bf;
171 | color: #1861bf;
172 |
173 | height: 40px;
174 | width: 40px;
175 | margin-left: 10px;
176 | margin-top: 10px;
177 | border-radius: 2px;
178 | `;
179 |
180 | export const IssueListContainer = styled.div`
181 | height: 100%;
182 | display: flex;
183 | padding: 10px;
184 | overflow: auto;
185 | word-wrap: break-word;
186 | flex-direction: column;
187 | `;
188 |
189 | export const Button = styled.button`
190 | color: #fff;
191 | border: none;
192 | cursor: pointer;
193 | padding: 6px 12px;
194 | border-radius: 5px;
195 | background: #1861bf;
196 | text-transform: capitalize;
197 |
198 | :hover {
199 | opacity: 0.8;
200 | }
201 | `;
202 |
203 | export const Title = styled.div`
204 | flex: 1;
205 | color: #fff;
206 | font-size: 16px;
207 | cursor: pointer;
208 | padding-left: 10px;
209 | text-transform: uppercase;
210 | `;
211 |
212 | export const Select = styled.select`
213 | padding: 5px;
214 | margin-right: 10px;
215 | border-radius: 2px;
216 | color: #fff;
217 | border: 0px;
218 | background-color: rgba(255, 255, 255, 0.125);
219 |
220 | option {
221 | border: 0px;
222 | }
223 |
224 | :focus {
225 | color: #444;
226 | border: 0px;
227 | background: #fff;
228 | }
229 | `;
230 |
231 | export const Loading = styled.div`
232 | width: 22px;
233 | height: 22px;
234 | margin: 10px auto;
235 | border-radius: 50px;
236 | border: 3px solid rgba(210,210,210,.7);
237 | border-left-color: #39BF80;
238 | display: ${(props) => (props.isLoading ? 'block' : 'none')}
239 | animation: donut-spin 1.2s linear infinite;
240 | `;
241 |
242 | export const Bold = styled.b`
243 | font-weight: 500;
244 | `;
245 |
246 | export const Message = styled.div`
247 | flex: 1;
248 | padding: 13px;
249 | background: #eee;
250 | color: #444;
251 | margin-top: 30px;
252 | border-radius: 2px;
253 | `;
254 |
255 | export const LoginButton = styled.button`
256 | color: #fff;
257 | padding: 3px 15px;
258 | font-size: 12px;
259 | line-height: 20px;
260 | border: 1px solid rgba(27, 31, 35, 0.2);
261 | border-radius: 3px;
262 | margin: 0px 10px;
263 | background: linear-gradient(-180deg, #34d058 0%, #28a745 90%);
264 | cursor: pointer;
265 | font-weight: 600;
266 |
267 | :hover {
268 | opacity: 0.9;
269 | }
270 | `;
271 |
272 | export const IssueComponent = styled.div`
273 | margin-top: 30px;
274 | `;
275 |
276 | export const TabContainer = styled.div`
277 | display: flex;
278 | `;
279 |
280 | export const RepoTab = styled.button`
281 | padding: 10px;
282 | font-size: 13px;
283 | cursor: pointer;
284 | color: ${(props) => (props.filted ? '#AAA' : '#FFF')};
285 | background: ${(props) => (props.filted ? '#DDD' : '#5c90d2')};
286 | border-radius: 5px;
287 | margin: 0px 10px 20px 0px;
288 | border: 1px solid ${(props) => (props.filted ? '#d1d5da' : '#FFF')};
289 | `;
290 |
291 | export const ShareButton = styled.button`
292 | color: #fff;
293 | border: none;
294 | padding: 10px;
295 | cursor: pointer;
296 | background: #fff;
297 | border-radius: 5px;
298 | background: #5c90d2;
299 | align-self: flex-end;
300 | justify-self: flex-end;
301 | `;
302 |
303 | export const ScrollContainer = styled.div`
304 | overflow-y: scroll;
305 | background: #FFF;
306 | height: calc(100vh - 215px);
307 | border: 1px solid #d1d5da;
308 | `;
309 |
310 | export const Empty = styled.div`
311 | display: flex;
312 | color: #585858;
313 | flex-direction: column;
314 | align-items: center;
315 | justify-content: center;
316 | height: 100%;
317 |
318 | h3 {
319 | font-size: 20px;
320 | margin-bottom: 0px;
321 | }
322 | `;
323 |
--------------------------------------------------------------------------------