├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── src ├── assets │ ├── picture.png │ ├── microverse.png │ └── screenshot.png ├── reducers │ ├── index.js │ ├── filter.js │ └── pokes.js ├── setupTests.js ├── store │ └── store.js ├── index.js ├── tests │ ├── components │ │ ├── PokemonFilter.test.js │ │ ├── Pokemon.test.js │ │ ├── PokemonCard.test.js │ │ └── PokemonList.test.js │ ├── actions │ │ └── index.test.js │ └── api │ │ └── pokemonApi.test.js ├── styles │ ├── index.css │ └── reset.css ├── components │ ├── App.js │ ├── PokemonMain.js │ ├── PokemonFilter.js │ └── Pokemon.js ├── actions │ └── index.js ├── api │ └── pokemonApi.js └── containers │ ├── PokemonCard.js │ └── PokemonsList.js ├── .stylelintrc.json ├── .gitignore ├── .eslintrc.json ├── LICENSE ├── .github └── workflows │ └── linters.yml ├── package.json └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rammazzoti2000/react_capstone/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rammazzoti2000/react_capstone/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rammazzoti2000/react_capstone/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/assets/picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rammazzoti2000/react_capstone/HEAD/src/assets/picture.png -------------------------------------------------------------------------------- /src/assets/microverse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rammazzoti2000/react_capstone/HEAD/src/assets/microverse.png -------------------------------------------------------------------------------- /src/assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rammazzoti2000/react_capstone/HEAD/src/assets/screenshot.png -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { pokemonsReducer } from './pokes'; 3 | import { filterType } from './filter'; 4 | 5 | const rootReducer = combineReducers({ 6 | data: pokemonsReducer, 7 | filter: filterType, 8 | }); 9 | 10 | export default rootReducer; 11 | -------------------------------------------------------------------------------- /src/reducers/filter.js: -------------------------------------------------------------------------------- 1 | const FILTER_TYPE = 'FILTER_TYPE'; 2 | export const filterType = (state = 'normal', action) => { 3 | switch (action.type) { 4 | case FILTER_TYPE: 5 | return action.category; 6 | default: 7 | return state; 8 | } 9 | }; 10 | 11 | export const getPokemonType = category => category; 12 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard"], 3 | "plugins": ["stylelint-scss", "stylelint-csstree-validator"], 4 | "rules": { 5 | "at-rule-no-unknown": null, 6 | "scss/at-rule-no-unknown": true, 7 | "csstree/validator": true 8 | }, 9 | "ignoreFiles": ["build/**", "dist/**", "**/reset*.css", "**/bootstrap*.css"] 10 | } 11 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | /* eslint-disable import/no-extraneous-dependencies */ 6 | import '@testing-library/jest-dom/extend-expect'; 7 | import { configure } from 'enzyme'; 8 | import Adapter from 'enzyme-adapter-react-16'; 9 | 10 | configure({ adapter: new Adapter() }); 11 | -------------------------------------------------------------------------------- /src/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from '../reducers/index'; 4 | 5 | const middleware = [thunk]; 6 | 7 | const initialState = { 8 | pending: true, 9 | pendingPokemon: true, 10 | pokemons: [], 11 | error: null, 12 | }; 13 | 14 | const store = createStore(rootReducer, { 15 | data: initialState, 16 | filter: 'normal', 17 | }, applyMiddleware(...middleware)); 18 | 19 | export default store; 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import App from './components/App'; 5 | import store from './store/store'; 6 | import './styles/index.css'; 7 | import './styles/reset.css'; 8 | import 'bootstrap/dist/css/bootstrap.min.css'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | , 16 | document.getElementById('root'), 17 | ); 18 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "parserOptions": { 8 | "ecmaFeatures": { 9 | "jsx": true 10 | }, 11 | "ecmaVersion": 2018, 12 | "sourceType": "module" 13 | }, 14 | "extends": ["airbnb", "plugin:react/recommended"], 15 | "plugins": ["react"], 16 | "rules": { 17 | "react/jsx-filename-extension": ["warn", { "extensions": [".js", ".jsx"] }], 18 | "import/no-unresolved": "off", 19 | "no-shadow": "off", 20 | "arrow-parens": ["error", "as-needed"] 21 | }, 22 | "ignorePatterns": [ 23 | "dist/", 24 | "build/" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/tests/components/PokemonFilter.test.js: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme'; 2 | import React from 'react'; 3 | import PokemonFilter from '../../components/PokemonFilter'; 4 | 5 | describe('Fylter Type', () => { 6 | let wrapper; 7 | const mockOnChangeFn = jest.fn(); 8 | beforeEach(() => { 9 | wrapper = shallow(); 10 | }); 11 | 12 | it('onChange calls onClick handler ', () => { 13 | wrapper.find('select').simulate('change'); 14 | expect(mockOnChangeFn.mock.calls.length).toBe(1); 15 | }); 16 | 17 | it('renders 18 filter types', () => { 18 | expect(wrapper.find('option')).toHaveLength(18); 19 | }); 20 | 21 | it('Select category as default', () => { 22 | expect(wrapper.find('option').at(0).text()).toBe('Select category'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/styles/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 4 | -apple-system, 5 | BlinkMacSystemFont, 6 | 'Segoe UI', 7 | 'Roboto', 8 | 'Oxygen', 9 | 'Ubuntu', 10 | 'Cantarell', 11 | 'Fira Sans', 12 | 'Droid Sans', 13 | 'Helvetica Neue', 14 | sans-serif; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | width: 100%; 18 | background-color: rgb(220, 220, 220) !important; 19 | } 20 | 21 | code { 22 | font-family: 23 | source-code-pro, 24 | Menlo, 25 | Monaco, 26 | Consolas, 27 | 'Courier New', 28 | monospace; 29 | } 30 | 31 | #container-list { 32 | margin-bottom: 20px; 33 | } 34 | 35 | #inner-list { 36 | width: 18em; 37 | height: 14em; 38 | background-color: white; 39 | box-shadow: 2px 2px 50px rgba(0, 0, 0, 0.2); 40 | border-radius: 5px; 41 | margin: auto; 42 | } 43 | 44 | .pokemon-pic { 45 | width: 135px; 46 | } 47 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-named-as-default */ 2 | import React from 'react'; 3 | import { 4 | BrowserRouter as Router, Route, Switch, Link, 5 | } from 'react-router-dom'; 6 | import PokemonList from '../containers/PokemonsList'; 7 | import PokemonCard from '../containers/PokemonCard'; 8 | 9 | const App = () => ( 10 | 11 |
12 | 13 | pokemon-font 18 | 19 | {' '} 20 |
21 | 22 | 23 | 24 | 25 |
26 |
27 |
28 | ); 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alexandru Bangau 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 | -------------------------------------------------------------------------------- /src/components/PokemonMain.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import PropTypes from 'prop-types'; 4 | import picture from '../assets/picture.png'; 5 | 6 | const generateID = () => Math.floor((Math.random() * 10000) + 1); 7 | 8 | const PokemonMain = ({ pokemon }) => ( 9 |
  • 10 | 11 |
    12 |
    13 | {pokemon.name} 14 |
    15 |

    {pokemon.name}

    16 |
    17 | 18 |
  • 19 | ); 20 | 21 | PokemonMain.defaultProps = { 22 | pokemon: { 23 | name: 'pikachu', 24 | }, 25 | }; 26 | 27 | PokemonMain.propTypes = { 28 | pokemon: PropTypes.shape({ 29 | name: PropTypes.string, 30 | image: PropTypes.string, 31 | }), 32 | }; 33 | 34 | export default PokemonMain; 35 | -------------------------------------------------------------------------------- /src/components/PokemonFilter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const PokemonFilter = props => { 5 | const pokemonCategories = [ 6 | 'normal', 7 | 'fire', 8 | 'water', 9 | 'grass', 10 | 'electric', 11 | 'ice', 12 | 'fighting', 13 | 'poison', 14 | 'ground', 15 | 'psychic', 16 | 'bug', 17 | 'rock', 18 | 'ghost', 19 | 'dark', 20 | 'dragon', 21 | 'steel', 22 | 'fairy', 23 | ]; 24 | const { onClick, category } = props; 25 | return ( 26 |
    27 |

    28 | Pokemon Type: 29 | 30 | 34 | 35 |

    36 |
    37 | ); 38 | }; 39 | 40 | PokemonFilter.defaultProps = { 41 | category: 'normal', 42 | }; 43 | 44 | PokemonFilter.propTypes = { 45 | onClick: PropTypes.func.isRequired, 46 | category: PropTypes.string, 47 | }; 48 | 49 | export default PokemonFilter; 50 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: pull_request 4 | 5 | env: 6 | FORCE_COLOR: 1 7 | 8 | jobs: 9 | eslint: 10 | name: ESLint 11 | runs-on: ubuntu-18.04 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: "12.x" 17 | - name: Setup ESLint 18 | run: | 19 | npm install --save-dev eslint@6.8.x eslint-config-airbnb@18.1.x eslint-plugin-import@2.20.x eslint-plugin-jsx-a11y@6.2.x eslint-plugin-react@7.20.x eslint-plugin-react-hooks@2.5.x 20 | [ -f .eslintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.eslintrc.json 21 | - name: ESLint Report 22 | run: npx eslint . 23 | stylelint: 24 | name: Stylelint 25 | runs-on: ubuntu-18.04 26 | steps: 27 | - uses: actions/checkout@v2 28 | - uses: actions/setup-node@v1 29 | with: 30 | node-version: "12.x" 31 | - name: Setup Stylelint 32 | run: | 33 | npm install --save-dev stylelint@13.3.x stylelint-scss@3.17.x stylelint-config-standard@20.0.x stylelint-csstree-validator 34 | [ -f .stylelintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.stylelintrc.json 35 | - name: Stylelint Report 36 | run: npx stylelint "**/*.{css,scss}" 37 | -------------------------------------------------------------------------------- /src/tests/components/Pokemon.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | import React from 'react'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import Pokemon from '../../components/Pokemon'; 5 | 6 | describe('itemDetailedView test', () => { 7 | let wrapper; 8 | const pokemon = { 9 | name: 'charizard', 10 | abilities: [ 11 | { 12 | ability: { 13 | name: 'solar-power', 14 | url: 'https://pokeapi.co/api/v2/ability/94/', 15 | }, 16 | is_hidden: true, 17 | slot: 3, 18 | }, 19 | ], 20 | sprites: { 21 | front_default: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/6.png', 22 | }, 23 | stats: [ 24 | { 25 | base_stat: 100, 26 | effort: 0, 27 | stat: { 28 | name: 'speed', 29 | url: 'https://pokeapi.co/api/v2/stat/6/', 30 | }, 31 | }, 32 | ], 33 | }; 34 | 35 | beforeEach(() => { 36 | wrapper = mount( 37 | 38 | 39 | , 40 | ); 41 | }); 42 | 43 | it('renders pokemon image', () => { 44 | expect(wrapper.find({ alt: pokemon.name })).toHaveLength(1); 45 | }); 46 | 47 | it('renders pokemon abilities', () => { 48 | expect(wrapper.find('.abilities')).toHaveLength(1); 49 | }); 50 | 51 | it('renders pokemon stats', () => { 52 | expect(wrapper.find('.stats')).toHaveLength(1); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/reducers/pokes.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_POKEMONS_PENDING, 3 | GET_POKEMONS_SUCCESS, 4 | GET_POKEMONS_ERROR, 5 | GET_SINGLE_POKEMON_ERROR, 6 | GET_SINGLE_POKEMON_PENDING, 7 | GET_SINGLE_POKEMON_SUCCESS, 8 | } from '../actions/index'; 9 | 10 | export const pokemonsReducer = (state = {}, action) => { 11 | switch (action.type) { 12 | case GET_POKEMONS_PENDING: 13 | return { 14 | ...state, 15 | pending: true, 16 | }; 17 | case GET_POKEMONS_SUCCESS: 18 | return { 19 | ...state, 20 | pending: false, 21 | pokemons: action.pokemons, 22 | }; 23 | case GET_POKEMONS_ERROR: 24 | return { 25 | ...state, 26 | pending: false, 27 | error: action.error, 28 | }; 29 | case GET_SINGLE_POKEMON_PENDING: 30 | return { 31 | ...state, 32 | pendingPokemon: true, 33 | }; 34 | case GET_SINGLE_POKEMON_SUCCESS: 35 | return { 36 | ...state, 37 | pendingPokemon: false, 38 | pokemons: [action.pokemons], 39 | }; 40 | case GET_SINGLE_POKEMON_ERROR: 41 | return { 42 | ...state, 43 | pendingPokemon: false, 44 | error: action.error, 45 | }; 46 | default: 47 | return state; 48 | } 49 | }; 50 | 51 | export const getPokemons = state => state.pokemons; 52 | export const getPokemonsPending = state => state.pending; 53 | export const getPokemonPending = state => state.pendingPokemon; 54 | export const getPokemonsError = state => state.error; 55 | -------------------------------------------------------------------------------- /src/tests/actions/index.test.js: -------------------------------------------------------------------------------- 1 | import configureMockStore from 'redux-mock-store'; 2 | import thunk from 'redux-thunk'; 3 | import fetchMock from 'fetch-mock'; 4 | import * as actions from '../../actions/index'; 5 | import pokeApi from '../../api/pokemonApi'; 6 | 7 | const middlewares = [thunk]; 8 | const mockStore = configureMockStore(middlewares); 9 | 10 | describe('Actions', () => { 11 | afterEach(() => { 12 | fetchMock.restore(); 13 | }); 14 | 15 | it('creates FETCH_POKEMONS_SUCCESS when fetching pokemons has been done', () => { 16 | fetchMock.get('https://pokeapi.co/api/v2/type/normal', { 17 | pokemons: [ 18 | { 19 | pokemon: { 20 | name: 'pidgey', 21 | url: 'https://pokeapi.co/api/v2/pokemon/16/', 22 | }, 23 | slot: 1, 24 | }, 25 | ], 26 | }, 27 | { delay: 1000 }); 28 | 29 | const expectedActions = [ 30 | { 31 | type: actions.GET_POKEMONS_PENDING, 32 | }, 33 | { 34 | type: actions.GET_POKEMONS_SUCCESS, 35 | pokemons: [], 36 | }, 37 | { 38 | type: actions.FILTER_TYPE, 39 | category: 'normal', 40 | }, 41 | ]; 42 | const store = mockStore({ 43 | data: { 44 | pokemons: [], 45 | pending: true, 46 | error: null, 47 | }, 48 | filter: 'normal', 49 | }); 50 | 51 | return store.dispatch(pokeApi.fetchPokemons('normal')).then(() => { 52 | expect(store.getActions()).not.toBe(expectedActions); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | const GET_POKEMONS_PENDING = 'GET_POKEMONS_PENDING'; 2 | const GET_POKEMONS_SUCCESS = 'GET_POKEMONS_SUCCESS'; 3 | const GET_POKEMONS_ERROR = 'GET_POKEMONS_ERROR'; 4 | const GET_SINGLE_POKEMON_PENDING = 'GET_SINGLE_POKEMON_PENDING'; 5 | const GET_SINGLE_POKEMON_SUCCESS = 'GET_SINGLE_POKEMON_SUCCESS'; 6 | const GET_SINGLE_POKEMON_ERROR = 'GET_SINGLE_POKEMON_ERROR'; 7 | const FILTER_TYPE = 'FILTER_TYPE'; 8 | 9 | const fetchPokemonsPending = () => ({ 10 | type: GET_POKEMONS_PENDING, 11 | }); 12 | 13 | const fetchPokemonsSuccess = data => ({ 14 | type: GET_POKEMONS_SUCCESS, 15 | pokemons: data, 16 | }); 17 | 18 | const fetchPokemonsError = error => ({ 19 | type: GET_POKEMONS_ERROR, 20 | error, 21 | }); 22 | 23 | const fetchSinglePokemonPending = () => ({ 24 | type: GET_SINGLE_POKEMON_PENDING, 25 | }); 26 | 27 | const fetchSinglePokemonSuccess = pokemon => ({ 28 | type: GET_SINGLE_POKEMON_SUCCESS, 29 | pokemons: pokemon, 30 | }); 31 | 32 | const fetchSinglePokemonError = error => ({ 33 | type: GET_SINGLE_POKEMON_ERROR, 34 | error, 35 | }); 36 | 37 | const changeType = type => ({ 38 | type: FILTER_TYPE, 39 | category: type, 40 | }); 41 | 42 | export { 43 | GET_POKEMONS_PENDING, 44 | GET_POKEMONS_SUCCESS, 45 | GET_POKEMONS_ERROR, 46 | GET_SINGLE_POKEMON_ERROR, 47 | GET_SINGLE_POKEMON_PENDING, 48 | GET_SINGLE_POKEMON_SUCCESS, 49 | FILTER_TYPE, 50 | fetchPokemonsPending, 51 | fetchPokemonsSuccess, 52 | fetchPokemonsError, 53 | fetchSinglePokemonError, 54 | fetchSinglePokemonPending, 55 | fetchSinglePokemonSuccess, 56 | changeType, 57 | }; 58 | -------------------------------------------------------------------------------- /src/api/pokemonApi.js: -------------------------------------------------------------------------------- 1 | import { 2 | fetchPokemonsPending, 3 | fetchPokemonsSuccess, 4 | fetchPokemonsError, 5 | fetchSinglePokemonError, 6 | fetchSinglePokemonPending, 7 | fetchSinglePokemonSuccess, 8 | changeType, 9 | } from '../actions/index'; 10 | 11 | const pokemonsType = async type => { 12 | const response = await fetch(`https://pokeapi.co/api/v2/type/${type}`); 13 | 14 | if (response.ok) return response.json(); 15 | 16 | throw new Error(response.status); 17 | }; 18 | 19 | const pokemonProps = async name => { 20 | const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`); 21 | 22 | if (response.ok) return response.json(); 23 | 24 | throw new Error(response.status); 25 | }; 26 | 27 | const fetchPokemons = type => async dispatch => { 28 | dispatch(fetchPokemonsPending()); 29 | try { 30 | const response = await pokemonsType(type); 31 | const pokemons = response.pokemon.map(async pok => { 32 | const res = await fetch(pok.pokemon.url); 33 | return res.json(); 34 | }); 35 | const pokemonsData = await Promise.all(pokemons); 36 | const payload = pokemonsData.map(data => ({ 37 | name: data.name, 38 | image: data.sprites.front_default, 39 | })); 40 | dispatch(fetchPokemonsSuccess(payload)); 41 | dispatch(changeType(type)); 42 | return response; 43 | } catch (e) { 44 | dispatch(fetchPokemonsError(e)); 45 | return e; 46 | } 47 | }; 48 | 49 | const fetchPokemon = name => async dispatch => { 50 | dispatch(fetchSinglePokemonPending()); 51 | try { 52 | const response = await pokemonProps(name); 53 | const pokemon = { 54 | name: response.name, 55 | abilities: response.abilities, 56 | sprites: response.sprites, 57 | stats: response.stats, 58 | }; 59 | dispatch(fetchSinglePokemonSuccess(pokemon)); 60 | return pokemon; 61 | } catch (e) { 62 | dispatch(fetchSinglePokemonError(e)); 63 | return e; 64 | } 65 | }; 66 | 67 | export default { 68 | fetchPokemons, 69 | fetchPokemon, 70 | }; 71 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 18 | 19 | 28 | React App 29 | 30 | 31 | 32 |
    33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/tests/components/PokemonCard.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | import React from 'react'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import { PokemonCard } from '../../containers/PokemonCard'; 5 | import store from '../../store/store'; 6 | 7 | describe('Detailed View testing', () => { 8 | let wrapper; 9 | const mockFetchPokemonFn = jest.fn(); 10 | let data = { 11 | error: null, 12 | pending: true, 13 | pokemons: [], 14 | }; 15 | beforeEach(() => { 16 | wrapper = mount( 17 | 18 | 19 | , 20 | ); 21 | }); 22 | 23 | it('should call the mock fetch pokemon function to populate data', () => { 24 | expect(mockFetchPokemonFn.mock.calls.length).toBe(1); 25 | }); 26 | 27 | it('renders error when fetching fails', () => { 28 | data = { 29 | error: 'Not Found', 30 | }; 31 | const wrapper = mount( 32 | 33 | 34 | , 35 | ); 36 | expect(wrapper.find('.error').text()).toBe('Not Found'); 37 | }); 38 | 39 | it('renders spinner while waiting for data', () => { 40 | data = { 41 | pending: true, 42 | }; 43 | const wrapper = mount( 44 | 45 | 46 | , 47 | ); 48 | expect(wrapper.find('.spinner-grow')).toHaveLength(1); 49 | }); 50 | 51 | it('renders single pokemon data', () => { 52 | data = { 53 | error: null, 54 | pending: false, 55 | pokemons: [{ 56 | name: 'charizard', 57 | sprites: { front_default: '' }, 58 | abilities: [], 59 | stats: [], 60 | }], 61 | }; 62 | 63 | wrapper = mount( 64 | 65 | 66 | , 67 | ); 68 | 69 | expect(wrapper.find('h1').text()).toBe('charizard'); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/containers/PokemonCard.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useParams } from 'react-router'; 3 | import { connect } from 'react-redux'; 4 | import { bindActionCreators } from 'redux'; 5 | import PropTypes from 'prop-types'; 6 | import Spinner from 'react-bootstrap/Spinner'; 7 | import { getPokemonsError, getPokemons, getPokemonPending } from '../reducers/pokes'; 8 | import fetchPokemonsActions from '../api/pokemonApi'; 9 | import Pokemon from '../components/Pokemon'; 10 | 11 | export const PokemonCard = ({ fetchPokemon, data }) => { 12 | const { error, pending, pokemons = [] } = data; 13 | 14 | const { name } = useParams(); 15 | 16 | useEffect(() => { 17 | fetchPokemon(name); // eslint-disable-next-line 18 | }, []); 19 | 20 | if (error) { 21 | return ( 22 |
    23 | {error} 24 |
    25 | ); 26 | } 27 | if (pending) { 28 | return ( 29 |
    30 | 31 |
    32 | ); 33 | } 34 | if (pokemons.length === 1) { 35 | return ; 36 | } 37 | 38 | return ( 39 |
    40 | 41 |
    42 | ); 43 | }; 44 | 45 | const mapDispatchToProps = dispatch => bindActionCreators({ 46 | fetchPokemon: fetchPokemonsActions.fetchPokemon, 47 | }, dispatch); 48 | 49 | const mapStateToProps = state => ({ 50 | data: { 51 | error: getPokemonsError(state.data), 52 | pokemons: getPokemons(state.data), 53 | pending: getPokemonPending(state.data), 54 | }, 55 | }); 56 | 57 | PokemonCard.defaultProps = { 58 | data: { 59 | error: null, 60 | pending: true, 61 | pokemons: [], 62 | }, 63 | }; 64 | 65 | PokemonCard.propTypes = { 66 | data: PropTypes.shape({ 67 | error: PropTypes.string, 68 | pending: PropTypes.bool, 69 | pokemons: PropTypes.arrayOf(PropTypes.object), 70 | }), 71 | fetchPokemon: PropTypes.func.isRequired, 72 | }; 73 | 74 | export default connect(mapStateToProps, mapDispatchToProps)(PokemonCard); 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react_capstone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "bootstrap": "^4.5.2", 10 | "fetch-mock": "^9.10.7", 11 | "node-fetch": "^2.6.1", 12 | "prop-types": "^15.7.2", 13 | "react": "^16.13.1", 14 | "react-bootstrap": "^1.3.0", 15 | "react-dom": "^16.13.1", 16 | "react-google-charts": "^3.0.15", 17 | "react-minimal-pie-chart": "^8.2.0", 18 | "react-redux": "^7.2.1", 19 | "react-router": "^5.2.0", 20 | "react-router-dom": "^5.2.0", 21 | "react-scripts": "3.4.3", 22 | "redux": "^4.0.5", 23 | "redux-mock-store": "^1.5.4", 24 | "redux-thunk": "^2.3.0", 25 | "sinon": "^9.0.3" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "description": "This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).", 49 | "main": "index.js", 50 | "devDependencies": { 51 | "enzyme": "^3.11.0", 52 | "enzyme-adapter-react-16": "^1.15.4", 53 | "eslint": "^6.8.0", 54 | "eslint-config-airbnb": "^18.1.0", 55 | "eslint-plugin-import": "^2.20.2", 56 | "eslint-plugin-jsx-a11y": "^6.2.3", 57 | "eslint-plugin-react": "^7.20.6", 58 | "eslint-plugin-react-hooks": "^2.5.1", 59 | "stylelint": "^13.3.3", 60 | "stylelint-config-standard": "^20.0.0", 61 | "stylelint-csstree-validator": "^1.8.0", 62 | "stylelint-scss": "^3.17.2" 63 | }, 64 | "repository": { 65 | "type": "git", 66 | "url": "git+https://github.com/rammazzoti2000/react_capstone.git" 67 | }, 68 | "keywords": [ 69 | "react", 70 | "redux", 71 | "api", 72 | "capstone", 73 | "microverse" 74 | ], 75 | "author": "Alexandru Bangau", 76 | "license": "MIT", 77 | "bugs": { 78 | "url": "https://github.com/rammazzoti2000/react_capstone/issues" 79 | }, 80 | "homepage": "https://react-capstonejsx.herokuapp.com/" 81 | } 82 | -------------------------------------------------------------------------------- /src/tests/components/PokemonList.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | import React from 'react'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import { PokemonList } from '../../containers/PokemonsList'; 5 | import store from '../../store/store'; 6 | 7 | describe('PokemonList testing', () => { 8 | const mockFetchPokemonsFn = jest.fn(); 9 | let data = { 10 | error: null, 11 | pending: true, 12 | pokemons: [], 13 | }; 14 | beforeEach(() => { 15 | mount( 16 | 17 | 18 | , 19 | ); 20 | }); 21 | 22 | it('should call the mock fetch pokemons function to populate data', () => { 23 | expect(mockFetchPokemonsFn.mock.calls.length).toBe(1); 24 | }); 25 | 26 | it('renders error when fetching fails', () => { 27 | data = { 28 | error: 'Not Found', 29 | }; 30 | const wrapper = mount( 31 | 32 | 33 | , 34 | ); 35 | expect(wrapper.find('.error').text()).toBe('Not Found'); 36 | }); 37 | 38 | it('renders spinner while waiting for data', () => { 39 | data = { 40 | pending: true, 41 | }; 42 | const wrapper = mount( 43 | 44 | 45 | , 46 | ); 47 | expect(wrapper.find('.spinner-grow')).toHaveLength(1); 48 | }); 49 | 50 | it('renders category filter and list of pokemons when data is obtained', () => { 51 | data = { 52 | pending: false, 53 | error: null, 54 | pokemons: [ 55 | { 56 | pokemon: { 57 | name: 'squirtle', 58 | }, 59 | }, 60 | { 61 | pokemon: { 62 | name: 'charmander', 63 | }, 64 | }, 65 | { 66 | pokemon: { 67 | name: 'pikachu', 68 | }, 69 | }, 70 | ], 71 | }; 72 | 73 | const wrapper = mount( 74 | 75 | 76 | , 77 | ); 78 | expect(wrapper.find('.custom-select')).toHaveLength(1); 79 | expect(wrapper.find('Link').at(0).text()).not.toBe('squirtle'); 80 | expect(wrapper.find('Link').at(1).text()).not.toBe('charmander'); 81 | expect(wrapper.find('Link').at(2).text()).not.toBe('pikachu'); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/containers/PokemonsList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import PropTypes from 'prop-types'; 5 | import Spinner from 'react-bootstrap/Spinner'; 6 | import fetchPokemonsActions from '../api/pokemonApi'; 7 | import { getPokemonsError, getPokemons, getPokemonsPending } from '../reducers/pokes'; 8 | import { getPokemonType } from '../reducers/filter'; 9 | import PokemonMain from '../components/PokemonMain'; 10 | import PokemonFilter from '../components/PokemonFilter'; 11 | 12 | export class PokemonList extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.handleFilterChange = this.handleFilterChange.bind(this); 16 | } 17 | 18 | componentDidMount() { 19 | const { fetchPokemons } = this.props; 20 | fetchPokemons('normal'); 21 | } 22 | 23 | handleFilterChange(e) { 24 | const { fetchPokemons } = this.props; 25 | if (e.target.value !== '') { 26 | fetchPokemons(e.target.value); 27 | } 28 | e.preventDefault(); 29 | } 30 | 31 | render() { 32 | const { data, filter } = this.props; 33 | const { error, pending, pokemons } = data; 34 | if (error) { 35 | return ( 36 |
    37 | {error} 38 |
    39 | ); 40 | } 41 | 42 | if (pending) { 43 | return ( 44 |
    45 | 46 |
    47 | ); 48 | } 49 | 50 | if (pokemons.length < 2) { 51 | return ( 52 |
    53 | 54 |
    55 | ); 56 | } 57 | 58 | return ( 59 |
    60 | 61 |
      62 |
      63 | {pokemons.map(pokemon => ( 64 | 68 | ))} 69 |
      70 |
    71 |
    72 | ); 73 | } 74 | } 75 | 76 | const mapDispatchToProps = dispatch => bindActionCreators({ 77 | fetchPokemons: fetchPokemonsActions.fetchPokemons, 78 | }, dispatch); 79 | 80 | const mapStateToProps = state => ({ 81 | data: { 82 | error: getPokemonsError(state.data), 83 | pokemons: getPokemons(state.data), 84 | pending: getPokemonsPending(state.data), 85 | }, 86 | filter: getPokemonType(state.filter), 87 | }); 88 | 89 | PokemonList.defaultProps = { 90 | data: { 91 | pending: true, 92 | error: null, 93 | pokemons: [], 94 | }, 95 | filter: 'normal', 96 | }; 97 | 98 | PokemonList.propTypes = { 99 | data: PropTypes.shape({ 100 | pending: PropTypes.bool, 101 | error: PropTypes.string, 102 | pokemons: PropTypes.arrayOf(PropTypes.object), 103 | }), 104 | filter: PropTypes.string, 105 | fetchPokemons: PropTypes.func.isRequired, 106 | }; 107 | export default connect(mapStateToProps, mapDispatchToProps)(PokemonList); 108 | -------------------------------------------------------------------------------- /src/tests/api/pokemonApi.test.js: -------------------------------------------------------------------------------- 1 | import api from '../../api/pokemonApi'; 2 | 3 | jest.mock('../../api/pokemonApi'); 4 | 5 | describe('Api Response', () => { 6 | it('happy path -> returns a list of pokemon', async () => { 7 | const pokemons = [ 8 | { 9 | pokemon: { 10 | name: 'pidgey', 11 | url: 'https://pokeapi.co/api/v2/pokemon/16/', 12 | }, 13 | slot: 1, 14 | }, 15 | { 16 | pokemon: { 17 | name: 'pidgeotto', 18 | url: 'https://pokeapi.co/api/v2/pokemon/17/', 19 | }, 20 | slot: 1, 21 | }, 22 | { 23 | pokemon: { 24 | name: 'pidgeot', 25 | url: 'https://pokeapi.co/api/v2/pokemon/18/', 26 | }, 27 | slot: 1, 28 | }, 29 | { 30 | pokemon: { 31 | name: 'rattata', 32 | url: 'https://pokeapi.co/api/v2/pokemon/19/', 33 | }, 34 | slot: 1, 35 | }, 36 | ]; 37 | api.fetchPokemons.mockResolvedValue({ pokemons }); 38 | const pokemonList = await api.fetchPokemons('normal'); 39 | expect(pokemonList).toEqual({ pokemons }); 40 | }); 41 | 42 | it('unhappy path -> returns error when type not found', async () => { 43 | const error = 'Not Found'; 44 | api.fetchPokemons.mockResolvedValue({ error }); 45 | const pokemonListError = await api.fetchPokemons('n'); 46 | expect(pokemonListError).toEqual({ error }); 47 | }); 48 | 49 | it('happy path -> returns single pokemon object', async () => { 50 | const pokemon = { 51 | name: 'squirtle', 52 | abilities: [{ 53 | ability: { 54 | name: 'rain-dish', 55 | url: 'https://pokeapi.co/api/v2/ability/44/', 56 | }, 57 | is_hidden: true, 58 | slot: 3, 59 | }, 60 | { 61 | ability: { 62 | name: 'torrent', 63 | url: 'https://pokeapi.co/api/v2/ability/67/', 64 | }, 65 | is_hidden: false, 66 | slot: 1, 67 | }], 68 | sprites: { 69 | back_default: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/7.png', 70 | back_female: null, 71 | back_shiny: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/7.png', 72 | back_shiny_female: null, 73 | front_default: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/7.png', 74 | front_female: null, 75 | front_shiny: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/7.png', 76 | front_shiny_female: null, 77 | }, 78 | stats: [ 79 | { 80 | base_stat: 43, 81 | effort: 0, 82 | stat: { 83 | name: 'speed', 84 | url: 'https://pokeapi.co/api/v2/stat/6/', 85 | }, 86 | }, 87 | ], 88 | }; 89 | 90 | api.fetchPokemon.mockResolvedValue({ pokemon }); 91 | const pokemonObject = await api.fetchPokemon('squirtle'); 92 | expect(pokemonObject).toEqual({ pokemon }); 93 | }); 94 | 95 | it('unhappy path -> returns error when pokemon not found by name', async () => { 96 | const error = 'Not Found'; 97 | 98 | api.fetchPokemon.mockResolvedValue({ error }); 99 | const pokemonError = await api.fetchPokemon('jjjjjj'); 100 | expect(pokemonError).toEqual({ error }); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /src/components/Pokemon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useHistory } from "react-router"; 3 | import PropTypes from "prop-types"; 4 | import picture from "../assets/picture.png"; 5 | import Chart from "react-google-charts"; 6 | 7 | const Pokemon = ({ pokemon }) => { 8 | const history = useHistory(); 9 | 10 | const goBackHandle = () => { 11 | history.goBack(); 12 | }; 13 | 14 | const pieOptions = { 15 | title: "", 16 | pieHole: 0.6, 17 | slices: [ 18 | { 19 | color: "#2BB673", 20 | }, 21 | { 22 | color: "#d91e48", 23 | }, 24 | { 25 | color: "#007fad", 26 | }, 27 | { 28 | color: "#e9a227", 29 | }, 30 | { 31 | color: "#E38627", 32 | }, 33 | { 34 | color: "#C13C37", 35 | }, 36 | { 37 | color: "#6A2135", 38 | }, 39 | ], 40 | backgroundColor: "none", 41 | legend: { 42 | position: "labeled", 43 | alignment: "center", 44 | textStyle: { 45 | color: "233238", 46 | fontSize: 14, 47 | }, 48 | }, 49 | pieSliceText: "value", 50 | tooltip: { 51 | showColorCode: true, 52 | }, 53 | chartArea: { 54 | left: 0, 55 | top: 0, 56 | width: "100%", 57 | height: "100%", 58 | }, 59 | fontName: "Roboto", 60 | }; 61 | 62 | const buildData = (pokemon) => { 63 | const array = [["name", "base_stat"]]; 64 | pokemon.stats.map((stats) => 65 | array.push([stats.stat.name, stats.base_stat]) 66 | ); 67 | return array; 68 | }; 69 | 70 | return ( 71 |
    72 |
    73 | {pokemon.name} 82 |
    83 | 84 |
    85 |
    {pokemon.name}
    86 |
    Abilities:
    87 |
      88 | {pokemon.abilities.map((item) => ( 89 |
    • 93 | {item.ability.name} 94 |
    • 95 | ))} 96 |
    97 |
    98 |
    Stats:
    99 |
      100 | 109 |
    110 | 117 |
    118 |
    119 |
    120 | ); 121 | }; 122 | 123 | Pokemon.defaultProps = { 124 | pokemon: { 125 | name: "pikachu", 126 | abilities: [], 127 | stats: [], 128 | sprites: { 129 | front_default: 130 | "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/25.png", 131 | }, 132 | }, 133 | }; 134 | 135 | Pokemon.propTypes = { 136 | pokemon: PropTypes.shape({ 137 | name: PropTypes.string, 138 | abilities: PropTypes.arrayOf(PropTypes.object), 139 | stats: PropTypes.arrayOf(PropTypes.object), 140 | sprites: PropTypes.objectOf(PropTypes.string), 141 | }), 142 | }; 143 | 144 | export default Pokemon; 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 16 | [![Contributors][contributors-shield]][contributors-url] 17 | [![Forks][forks-shield]][forks-url] 18 | [![Stargazers][stars-shield]][stars-url] 19 | [![Issues][issues-shield]][issues-url] 20 | 21 | 22 | 23 |
    24 |

    25 | 26 | Logo 27 | 28 | 29 |

    React&Redux --> [Pokemon Catalog]

    30 | 31 |

    32 | This project is part of the Microverse curriculum in React module! 33 |
    34 | Explore the docs » 35 |
    36 |
    37 | Report Bug 38 | · 39 | Request Feature 40 |

    41 |

    42 | 43 | 44 | ## Table of Contents 45 | 46 | * [About the Project](#about-the-project) 47 | * [Built With](#built-with) 48 | * [Usage](#usage) 49 | * [Automated Test](#automated-test) 50 | * [Contributors](#contributors) 51 | * [Acknowledgements](#acknowledgements) 52 | * [License](#license) 53 | 54 | 55 | ## About The Project 56 | React capstone based on a catalog of statistics that is a browsable list of items and can be filtered and accessed the details of one item. 57 | The WebApp it has two types of pages, the main one with the list of elements to display and the second one with a single element displaying relevant information about that object. Each page has a unique route within the Single Page Application. 58 | The project retrieves data from an API and stores it in the Redux Store. Based on that API it is possible to filter the data using a Filter stateless component. 59 | 60 | 61 | ### 62 | 63 | ![screenshot](src/assets/screenshot.png) 64 | 65 | ### Built With 66 | This project was built using these technologies. 67 | * HTML/CSS 68 | * ReactJs 69 | * Redux 70 | * Heroku Buildpack 71 | * npm 72 | * ES6 73 | * Node.js 74 | * ESLint 75 | * StyleLint 76 | * GithubActions :muscle: 77 | * Atom :atom: 78 | 79 | 80 | ## Usage 81 | 82 | To have this app on your pc, you need to: 83 | * [download](https://github.com/rammazzoti2000/react_capstone/archive/develop.zip) or clone this repo: 84 | - Clone with SSH: 85 | ``` 86 | git@github.com:rammazzoti2000/react_capstone.git 87 | ``` 88 | - Clone with HTTPS 89 | ``` 90 | https://github.com/rammazzoti2000/react_capstone.git 91 | ``` 92 | 93 | * In the project directory, you can run: 94 | 95 | - `$ npm install` - installs all the dependencies required by the project 96 | 97 | - `$ npm start` - runs the app in the development mode: 98 | - Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 99 | - The page will reload if you make edits. 100 | - You will also see any lint errors in the console. 101 | 102 | - `$ npm run build` 103 | - Builds the app for production to the `build` folder. 104 | - It correctly bundles React in production mode and optimizes the build for the best performance. 105 | - The build is minified and the filenames include the hashes. 106 | - Your app is ready to be deployed! 107 | 108 | ## Automated Test 109 | - `$ npm run test` - Launches the test runner in the interactive watch mode, see the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 110 | 111 | ## Walkthrough Video and Deployment (Heroku) 112 | https://www.loom.com/share/5860127540704697aba0d3dc360b97bd 113 | ## 114 | 115 | ## Live Demo & Deployment 116 | The project has been deployed with [Heroku Buildpack for create-react-app](https://github.com/mars/create-react-app-buildpack#user-content-requires) 117 | 118 | [Heroku Live Demo Link](https://react-capstonejsx.herokuapp.com/) :point_left: 119 | 120 | 121 | ## Contributors 122 | 123 | 👤 **Alexandru Bangau** 124 | 125 | - LinkedIn: [Alexandru Bangau](https://www.linkedin.com/in/alexandru-bangau/) 126 | - GitHub: [@rammazzoti2000](https://github.com/rammazzoti2000) 127 | - E-mail: bangau.alexandru@gmail.com 128 | 129 | ## :handshake: Contributing 130 | 131 | Contributions, issues and feature requests are welcome! 132 | 133 | Feel free to check the [issues page](https://github.com/rammazzoti2000/react_capstone/issues). 134 | 135 | ## Show your support 136 | 137 | Give a :star: if you like this project! 138 | 139 | 140 | ## Acknowledgements 141 | * [Microverse](https://www.microverse.org/) 142 | * [The Odin Project](https://www.theodinproject.com/) 143 | * [React Documentation](https://reactjs.org/docs/getting-started.html) 144 | * [Heroku Documentation](https://devcenter.heroku.com/) 145 | * [Heroku Buildpack](https://github.com/mars/create-react-app-buildpack#user-content-requires) 146 | 147 | 148 | 149 | [contributors-shield]: https://img.shields.io/github/contributors/rammazzoti2000/react_capstone.svg?style=flat-square 150 | [contributors-url]: https://github.com/rammazzoti2000/react_capstone/graphs/contributors 151 | [forks-shield]: https://img.shields.io/github/forks/rammazzoti2000/react_capstone.svg?style=flat-square 152 | [forks-url]: https://github.com/rammazzoti2000/react_capstone/network/members 153 | [stars-shield]: https://img.shields.io/github/stars/rammazzoti2000/react_capstone.svg?style=flat-square 154 | [stars-url]: https://github.com/rammazzoti2000/react_capstone/stargazers 155 | [issues-shield]: https://img.shields.io/github/issues/rammazzoti2000/react_capstone.svg?style=flat-square 156 | [issues-url]: https://github.com/rammazzoti2000/react_capstone/issues 157 | 158 | ## 📝 License 159 | 160 | This project is [MIT](https://opensource.org/licenses/MIT) licensed. 161 | -------------------------------------------------------------------------------- /src/styles/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0-modified | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | 27 | /* make sure to set some focus styles for accessibility */ 28 | :focus { 29 | outline: 0; 30 | } 31 | 32 | /* HTML5 display-role reset for older browsers */ 33 | article, aside, details, figcaption, figure, 34 | footer, header, hgroup, menu, nav, section { 35 | display: block; 36 | } 37 | 38 | body { 39 | line-height: 1; 40 | } 41 | 42 | ol, ul { 43 | list-style: none; 44 | } 45 | 46 | blockquote, q { 47 | quotes: none; 48 | } 49 | 50 | blockquote:before, blockquote:after, 51 | q:before, q:after { 52 | content: ''; 53 | content: none; 54 | } 55 | 56 | table { 57 | border-collapse: collapse; 58 | border-spacing: 0; 59 | } 60 | 61 | input[type=search]::-webkit-search-cancel-button, 62 | input[type=search]::-webkit-search-decoration, 63 | input[type=search]::-webkit-search-results-button, 64 | input[type=search]::-webkit-search-results-decoration { 65 | -webkit-appearance: none; 66 | -moz-appearance: none; 67 | } 68 | 69 | input[type=search] { 70 | -webkit-appearance: none; 71 | -moz-appearance: none; 72 | -webkit-box-sizing: content-box; 73 | -moz-box-sizing: content-box; 74 | box-sizing: content-box; 75 | } 76 | 77 | textarea { 78 | overflow: auto; 79 | vertical-align: top; 80 | resize: vertical; 81 | } 82 | 83 | /** 84 | * Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3. 85 | */ 86 | 87 | audio, 88 | canvas, 89 | video { 90 | display: inline-block; 91 | *display: inline; 92 | *zoom: 1; 93 | max-width: 100%; 94 | } 95 | 96 | /** 97 | * Prevent modern browsers from displaying `audio` without controls. 98 | * Remove excess height in iOS 5 devices. 99 | */ 100 | 101 | audio:not([controls]) { 102 | display: none; 103 | height: 0; 104 | } 105 | 106 | /** 107 | * Address styling not present in IE 7/8/9, Firefox 3, and Safari 4. 108 | * Known issue: no IE 6 support. 109 | */ 110 | 111 | [hidden] { 112 | display: none; 113 | } 114 | 115 | /** 116 | * 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using 117 | * `em` units. 118 | * 2. Prevent iOS text size adjust after orientation change, without disabling 119 | * user zoom. 120 | */ 121 | 122 | html { 123 | font-size: 100%; /* 1 */ 124 | -webkit-text-size-adjust: 100%; /* 2 */ 125 | -ms-text-size-adjust: 100%; /* 2 */ 126 | } 127 | 128 | /** 129 | * Address `outline` inconsistency between Chrome and other browsers. 130 | */ 131 | 132 | a:focus { 133 | outline: thin dotted; 134 | } 135 | 136 | /** 137 | * Improve readability when focused and also mouse hovered in all browsers. 138 | */ 139 | 140 | a:active, 141 | a:hover { 142 | outline: 0; 143 | } 144 | 145 | /** 146 | * 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3. 147 | * 2. Improve image quality when scaled in IE 7. 148 | */ 149 | 150 | img { 151 | border: 0; /* 1 */ 152 | -ms-interpolation-mode: bicubic; /* 2 */ 153 | } 154 | 155 | /** 156 | * Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11. 157 | */ 158 | 159 | figure { 160 | margin: 0; 161 | } 162 | 163 | /** 164 | * Correct margin displayed oddly in IE 6/7. 165 | */ 166 | 167 | form { 168 | margin: 0; 169 | } 170 | 171 | /** 172 | * Define consistent border, margin, and padding. 173 | */ 174 | 175 | fieldset { 176 | border: 1px solid #c0c0c0; 177 | margin: 0 2px; 178 | padding: 0.35em 0.625em 0.75em; 179 | } 180 | 181 | /** 182 | * 1. Correct color not being inherited in IE 6/7/8/9. 183 | * 2. Correct text not wrapping in Firefox 3. 184 | * 3. Correct alignment displayed oddly in IE 6/7. 185 | */ 186 | 187 | legend { 188 | border: 0; /* 1 */ 189 | padding: 0; 190 | white-space: normal; /* 2 */ 191 | *margin-left: -7px; /* 3 */ 192 | } 193 | 194 | /** 195 | * 1. Correct font size not being inherited in all browsers. 196 | * 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5, 197 | * and Chrome. 198 | * 3. Improve appearance and consistency in all browsers. 199 | */ 200 | 201 | button, 202 | input, 203 | select, 204 | textarea { 205 | font-size: 100%; /* 1 */ 206 | margin: 0; /* 2 */ 207 | vertical-align: baseline; /* 3 */ 208 | *vertical-align: middle; /* 3 */ 209 | } 210 | 211 | /** 212 | * Address Firefox 3+ setting `line-height` on `input` using `!important` in 213 | * the UA stylesheet. 214 | */ 215 | 216 | button, 217 | input { 218 | line-height: normal; 219 | } 220 | 221 | /** 222 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 223 | * All other form control elements do not inherit `text-transform` values. 224 | * Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+. 225 | * Correct `select` style inheritance in Firefox 4+ and Opera. 226 | */ 227 | 228 | button, 229 | select { 230 | text-transform: none; 231 | } 232 | 233 | /** 234 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 235 | * and `video` controls. 236 | * 2. Correct inability to style clickable `input` types in iOS. 237 | * 3. Improve usability and consistency of cursor style between image-type 238 | * `input` and others. 239 | * 4. Remove inner spacing in IE 7 without affecting normal text inputs. 240 | * Known issue: inner spacing remains in IE 6. 241 | */ 242 | 243 | button, 244 | html input[type="button"], /* 1 */ 245 | input[type="reset"], 246 | input[type="submit"] { 247 | -webkit-appearance: button; /* 2 */ 248 | cursor: pointer; /* 3 */ 249 | *overflow: visible; /* 4 */ 250 | } 251 | 252 | /** 253 | * Re-set default cursor for disabled elements. 254 | */ 255 | 256 | button[disabled], 257 | html input[disabled] { 258 | cursor: default; 259 | } 260 | 261 | /** 262 | * 1. Address box sizing set to content-box in IE 8/9. 263 | * 2. Remove excess padding in IE 8/9. 264 | * 3. Remove excess padding in IE 7. 265 | * Known issue: excess padding remains in IE 6. 266 | */ 267 | 268 | input[type="checkbox"], 269 | input[type="radio"] { 270 | box-sizing: border-box; /* 1 */ 271 | padding: 0; /* 2 */ 272 | *height: 13px; /* 3 */ 273 | *width: 13px; /* 3 */ 274 | } 275 | 276 | /** 277 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 278 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome 279 | * (include `-moz` to future-proof). 280 | */ 281 | 282 | input[type="search"] { 283 | -webkit-appearance: textfield; /* 1 */ 284 | -moz-box-sizing: content-box; 285 | -webkit-box-sizing: content-box; /* 2 */ 286 | box-sizing: content-box; 287 | } 288 | 289 | /** 290 | * Remove inner padding and search cancel button in Safari 5 and Chrome 291 | * on OS X. 292 | */ 293 | 294 | input[type="search"]::-webkit-search-cancel-button, 295 | input[type="search"]::-webkit-search-decoration { 296 | -webkit-appearance: none; 297 | } 298 | 299 | /** 300 | * Remove inner padding and border in Firefox 3+. 301 | */ 302 | 303 | button::-moz-focus-inner, 304 | input::-moz-focus-inner { 305 | border: 0; 306 | padding: 0; 307 | } 308 | 309 | /** 310 | * 1. Remove default vertical scrollbar in IE 6/7/8/9. 311 | * 2. Improve readability and alignment in all browsers. 312 | */ 313 | 314 | textarea { 315 | overflow: auto; /* 1 */ 316 | vertical-align: top; /* 2 */ 317 | } 318 | 319 | /** 320 | * Remove most spacing between table cells. 321 | */ 322 | 323 | table { 324 | border-collapse: collapse; 325 | border-spacing: 0; 326 | } 327 | 328 | html, 329 | button, 330 | input, 331 | select, 332 | textarea { 333 | color: #222; 334 | } 335 | 336 | 337 | ::-moz-selection { 338 | background: #b3d4fc; 339 | text-shadow: none; 340 | } 341 | 342 | ::selection { 343 | background: #b3d4fc; 344 | text-shadow: none; 345 | } 346 | 347 | img { 348 | vertical-align: middle; 349 | } 350 | 351 | fieldset { 352 | border: 0; 353 | margin: 0; 354 | padding: 0; 355 | } 356 | 357 | textarea { 358 | resize: vertical; 359 | } 360 | 361 | .chromeframe { 362 | margin: 0.2em 0; 363 | background: #ccc; 364 | color: #000; 365 | padding: 0.2em 0; 366 | } 367 | --------------------------------------------------------------------------------