├── 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 | Bem-vindo 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 | 14 | 15 | 16 | 17 | 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 |