diff --git a/package-lock.json b/package-lock.json index 12b4b6f..81097ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3982,6 +3982,11 @@ } } }, + "string_decoder": { + "version": "0.10.31", + "bundled": true, + "dev": true + }, "string-width": { "version": "1.0.1", "bundled": true, @@ -3992,11 +3997,6 @@ "strip-ansi": "3.0.1" } }, - "string_decoder": { - "version": "0.10.31", - "bundled": true, - "dev": true - }, "stringstream": { "version": "0.0.5", "bundled": true, @@ -7283,6 +7283,11 @@ "integrity": "sha1-DPf4T5Rj/wrlHExLFC2VvjdyTZw=", "dev": true }, + "ramda": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.25.0.tgz", + "integrity": "sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ==" + }, "randomatic": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", @@ -8265,6 +8270,15 @@ "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", "dev": true }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -8276,15 +8290,6 @@ "strip-ansi": "3.0.1" } }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, "stringstream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", diff --git a/package.json b/package.json index 493ce17..1bc62cf 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "faker": "*", "jquery": "*", + "ramda": "^0.25.0", "react": "*", "react-dom": "*", "react-redux": "*", diff --git a/src/App.css b/src/App.css deleted file mode 100644 index 15adfdc..0000000 --- a/src/App.css +++ /dev/null @@ -1,24 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - animation: App-logo-spin infinite 20s linear; - height: 80px; -} - -.App-header { - background-color: #222; - height: 150px; - padding: 20px; - color: white; -} - -.App-intro { - font-size: large; -} - -@keyframes App-logo-spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} diff --git a/src/App.js b/src/App.js deleted file mode 100644 index a3e9429..0000000 --- a/src/App.js +++ /dev/null @@ -1,42 +0,0 @@ -import React, { Component } from 'react'; -import { connect } from 'react-redux' -import { favouritePokemon, getPokemon } from './actions'; - -import {PokemonView} from './components/pokemon-view'; - -import { GET_ALL_POKEMON_URL } from './constants/api-url'; - -//https://img.pokemondb.net/artwork/${pokemon}.jpg <-- use for pictures - -const mapStateToProps = state => ({ - pokemon: state.pokemon.list, -}); - -const mapDispatchToProps = { - favouritePokemon, - getPokemon -}; - -class App extends Component { - componentDidMount() { - this.props.getPokemon(); - } - - render() { - - const { pokemon, favouritePokemon } = this.props; // go over destructuring again - return ( - -
-

Pokemon App!

- console.log(name)} pokeData={{name: 'Della', height: '180cm'}} /> - - {pokemon.map(poke =>

{poke.name}

)} - -
- //create PokemonList Here - ); - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(App); diff --git a/src/App.test.js b/src/App.test.js deleted file mode 100644 index 1f9aafe..0000000 --- a/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -// import React from 'react'; -// import ReactDOM from 'react-dom'; -// import App from './App'; -// -// it('renders without crashing', () => { -// const div = document.createElement('div'); -// ReactDOM.render(, div); -// }); diff --git a/src/actions/index.js b/src/actions/index.js deleted file mode 100644 index 2b3eed3..0000000 --- a/src/actions/index.js +++ /dev/null @@ -1,37 +0,0 @@ -import $ from 'jquery'; - -import {GET_ALL_POKEMON_URL} from '../constants/api-url'; - - -export const ACTION_TYPES = { - favouritePokemon: 'FAVOURITE_POKEMON', - setPokemon: 'SET_POKEMON' -}; - -export function favouritePokemon(pokemon) { - return { - type: ACTION_TYPES.favouritePokemon, - payload: { - pokemon, - } - } -} - - -export function getPokemon() { - - - return function (dispatch) { - $.get(GET_ALL_POKEMON_URL) - .then(response => { - dispatch({ - type: ACTION_TYPES.setPokemon, - payload: { - pokemon: response.results - } - }) - }); - - }; - -} diff --git a/src/components/clickable-list.js b/src/components/clickable-list.js new file mode 100644 index 0000000..286ec11 --- /dev/null +++ b/src/components/clickable-list.js @@ -0,0 +1,15 @@ +import React from 'react'; + +// Notice how this clickable list component doesn't know anything about pokemons. +// All it cares about is that it's a list, and to fire the onClickItem function +// passed to it when the user clicks an li. + +export const ClickableList = ({ data, onClickItem }) => ( + +); diff --git a/src/components/pokemon-view.js b/src/components/pokemon-view.js deleted file mode 100644 index def3284..0000000 --- a/src/components/pokemon-view.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -export const PokemonView = ({pokeData, favourite}) =>
- -

{pokeData.name}

- -

Height: {pokeData.height}

- - - -
; diff --git a/src/components/pokemon-view.test.js b/src/components/pokemon-view.test.js deleted file mode 100644 index f269e44..0000000 --- a/src/components/pokemon-view.test.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import { expect, assert } from 'chai'; -import { shallow } from 'enzyme'; -import {PokemonView} from './pokemon-view'; -import sinon from 'sinon'; - - -describe('', () => { - - it('Should display the name in an H2', () => { - - const wrapper = shallow(); - - expect(wrapper.find('h2')).to.have.length(1); - expect(wrapper.find('h2').text()).to.contain('Bob'); - - }); - - it('should favourite a pokemon on button click', () => { - - let mySpy = sinon.spy(); - - const wrapper = shallow(); - - - expect(mySpy.calledOnce).to.equal(false); - wrapper.find('button').simulate('click'); - expect(mySpy.calledOnce).to.equal(true); - - assert(mySpy.calledWith('Jon'), 'I expect the pokemon name that is passed in to be what is called when the button is clicked'); - - }); - -}); diff --git a/src/constants/api-url.js b/src/constants/api-url.js index 0dea808..877c9f6 100644 --- a/src/constants/api-url.js +++ b/src/constants/api-url.js @@ -1,3 +1 @@ -const POKEAPI_BASE_URL = 'https://pokeapi.co/api/v2/'; - -export const GET_ALL_POKEMON_URL = POKEAPI_BASE_URL + 'pokemon'; \ No newline at end of file +export const POKEAPI_BASE_URL = 'https://pokeapi.co/api/v2'; diff --git a/src/features/app/app.actions.js b/src/features/app/app.actions.js new file mode 100644 index 0000000..6236e14 --- /dev/null +++ b/src/features/app/app.actions.js @@ -0,0 +1,36 @@ +// Remember, thunks are just functions that return another function. +// It is not super obvious that fetchPokemons is a thunk, since it looks like it's just a function +// calling another function. However, pokemonRequest actually returns a function, see why in the expansions below. +// (2 & 3 are not part of the solution) + +import { pokemonRequest } from '../../utils/pokemonRequest'; +import { FETCH_POKEMONS } from '../pokemon-list/pokemon-list.types'; + +// Solution: +export const fetchPokemons = () => + pokemonRequest({ type: FETCH_POKEMONS, endpoint: 'pokemon' }); + +// Solution 2. Adds return statement to see that does have a return value. +export const fetchPokemons2 = () => { + return pokemonRequest({ type: FETCH_POKEMONS, endpoint: 'pokemon' }); +}; + +// Solution 3. Pastes values and pokemonRequest function to see how its a thunk. +export const fetchPokemons3 = () => { + return dispatch => { + dispatch({ type: FETCH_POKEMONS.START }); + return fetch('https://pokeapi.co/api/v2/pokemon') + .then(res => res.json()) + .then( + data => { + dispatch({ type: FETCH_POKEMONS.SUCCESS, payload: data }); + }, + error => { + dispatch({ type: FETCH_POKEMONS.FAILURE }); + }, + ) + .catch(eror => { + dispatch({ type: FETCH_POKEMONS.FAILURE }); + }); + }; +}; diff --git a/src/features/app/app.component.js b/src/features/app/app.component.js new file mode 100644 index 0000000..abfbf57 --- /dev/null +++ b/src/features/app/app.component.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { PokemonCard } from '../pokemon-card/pokemon-card.component'; +import { PokemonListContainer } from '../pokemon-list/pokemon-list.container'; + +export const App = ({ hasFavouritePokemon, favouritePokemon }) => ( +
+

Pokemon App!

+ {hasFavouritePokemon && } + +
+); diff --git a/src/features/app/app.container.js b/src/features/app/app.container.js new file mode 100644 index 0000000..b4d83de --- /dev/null +++ b/src/features/app/app.container.js @@ -0,0 +1,16 @@ +import { connect } from 'react-redux'; +import { App } from './app.component'; +import { isNil } from 'ramda'; + +// Check out ramda for some awesome utily functions! +// We could have also used "double bang" (!!) to get a boolean value +// (I try to avoid "truthy" and "falsy" values) + +const mapStateToProps = ({ pokemon }) => ({ + hasFavouritePokemon: !isNil(pokemon.favouritePokemon), + favouritePokemon: pokemon.favouritePokemon, +}); + +// Notice how my "connected" or "container" components don't have any markup, this is a best practice. + +export const AppContainer = connect(mapStateToProps)(App); diff --git a/src/features/pokemon-card/pokemon-card.component.js b/src/features/pokemon-card/pokemon-card.component.js new file mode 100644 index 0000000..0b4f593 --- /dev/null +++ b/src/features/pokemon-card/pokemon-card.component.js @@ -0,0 +1,12 @@ +import React from 'react'; + +export const PokemonCard = ({ pokemonName = 'Abdella', height = '183 cm' }) => ( +
+

{pokemonName}

+ pokemon +

Height: {height}

+
+); diff --git a/src/features/pokemon-list/pokemon-list.actions.js b/src/features/pokemon-list/pokemon-list.actions.js new file mode 100644 index 0000000..b33f967 --- /dev/null +++ b/src/features/pokemon-list/pokemon-list.actions.js @@ -0,0 +1,10 @@ +import { UPDATE_FAVOURITE_POKEMON } from './pokemon-list.types'; + +export function createUpdateFavouritePokemonAction(pokemonName) { + return { + type: UPDATE_FAVOURITE_POKEMON, + payload: { + pokemonName, + }, + }; +} diff --git a/src/features/pokemon-list/pokemon-list.container.js b/src/features/pokemon-list/pokemon-list.container.js new file mode 100644 index 0000000..e6eacf2 --- /dev/null +++ b/src/features/pokemon-list/pokemon-list.container.js @@ -0,0 +1,20 @@ +import { ClickableList } from '../../components/clickable-list'; +import { connect } from 'react-redux'; +import { createUpdateFavouritePokemonAction } from './pokemon-list.actions'; + +const mapStateToProps = state => ({ + data: state.pokemon.list.map(item => ({ + title: item.name, + })), +}); + +const mapDispatchToProps = dispatch => ({ + onClickItem: item => dispatch(createUpdateFavouritePokemonAction(item.title)), +}); + +// Notice how my "connected" or "container" components don't have any markup, this is a best practice. + +export const PokemonListContainer = connect( + mapStateToProps, + mapDispatchToProps, +)(ClickableList); diff --git a/src/features/pokemon-list/pokemon-list.reducer.js b/src/features/pokemon-list/pokemon-list.reducer.js new file mode 100644 index 0000000..f3c288b --- /dev/null +++ b/src/features/pokemon-list/pokemon-list.reducer.js @@ -0,0 +1,21 @@ +import { UPDATE_FAVOURITE_POKEMON, FETCH_POKEMONS } from './pokemon-list.types'; + +// Some features need their own reducer, like this one! + +const INITIAL_STATE = { + list: [], + favouritePokemon: null, +}; + +export const pokemonReducer = (state = INITIAL_STATE, { type, payload }) => { + switch (type) { + case UPDATE_FAVOURITE_POKEMON: + return { ...state, ...{ favouritePokemon: payload.pokemonName } }; + + case FETCH_POKEMONS.SUCCESS: + return { ...state, ...{ list: payload.results } }; + + default: + return state; + } +}; diff --git a/src/features/pokemon-list/pokemon-list.types.js b/src/features/pokemon-list/pokemon-list.types.js new file mode 100644 index 0000000..8f05afc --- /dev/null +++ b/src/features/pokemon-list/pokemon-list.types.js @@ -0,0 +1,11 @@ +export const UPDATE_FAVOURITE_POKEMON = 'FAVOURITE_POKEMON'; + +// FETCH_POKEMONS is an example of a pattern you might see with async redux types. +// It's not a rule, but when used with pokemonRequest, it can be easy to make hook +// up redux to our backend api's. + +export const FETCH_POKEMONS = { + START: 'FETCH_POKEMONS_START', + SUCCESS: 'FETCH_POKEMONS_SUCCESS', + FAILURE: 'FETCH_POKEMONS_FAILURE', +}; diff --git a/src/index.js b/src/index.js index 879c7de..50f17b1 100644 --- a/src/index.js +++ b/src/index.js @@ -1,20 +1,29 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { createStore, applyMiddleware } from 'redux'; +import { AppContainer } from './features/app/app.container'; +import { fetchPokemons } from './features/app/app.actions'; +import { createStore, applyMiddleware, compose } from 'redux'; import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; - -import App from './App'; +import { rootReducer } from './reducers'; import './index.css'; -import reducer from './reducers'; -const store = createStore(reducer, - applyMiddleware(thunk), // this is how thunk is integrated into the redux library - window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()); +const store = createStore( + rootReducer, + compose( + applyMiddleware(thunk), + window.__REDUX_DEVTOOLS_EXTENSION__ && + window.__REDUX_DEVTOOLS_EXTENSION__(), + ), +); ReactDOM.render( - - + + , - document.getElementById('root') + document.getElementById('root'), ); + +// You can also use the component did mount hook to make this ajax call in the App component. +// But since this also only runs once, I figured it was easier to fire it here. +store.dispatch(fetchPokemons()); diff --git a/src/reducers.js b/src/reducers.js new file mode 100644 index 0000000..2d30d34 --- /dev/null +++ b/src/reducers.js @@ -0,0 +1,6 @@ +import { combineReducers } from 'redux'; +import { pokemonReducer } from './features/pokemon-list/pokemon-list.reducer'; + +export const rootReducer = combineReducers({ + pokemon: pokemonReducer, +}); diff --git a/src/reducers/index.js b/src/reducers/index.js deleted file mode 100644 index a4f7478..0000000 --- a/src/reducers/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import { combineReducers } from 'redux'; -import {pokemon} from "./pokemon"; - - -export default combineReducers({ - pokemon, -}); diff --git a/src/reducers/pokemon.js b/src/reducers/pokemon.js deleted file mode 100644 index dd4da40..0000000 --- a/src/reducers/pokemon.js +++ /dev/null @@ -1,23 +0,0 @@ -import {ACTION_TYPES} from "../actions/index"; - - -const INITIAL_STATE = { - list: [], - favouritePokemon: null -}; - -export const pokemon = (state = INITIAL_STATE, {type, payload}) => { - - switch (type) { - case ACTION_TYPES.favouritePokemon: - return {...state, ...{favouritePokemon: payload.pokemon}}; - - case ACTION_TYPES.setPokemon: - - return {...state, ...{list: payload.pokemon}}; - - default: - return state; - } - -}; \ No newline at end of file diff --git a/src/utils/pokemonRequest.js b/src/utils/pokemonRequest.js new file mode 100644 index 0000000..5620b61 --- /dev/null +++ b/src/utils/pokemonRequest.js @@ -0,0 +1,27 @@ +import { POKEAPI_BASE_URL } from '../constants/api-url'; + +// This is a thunk! It's a function that returns another function! +// Here it makes an ajax request, and dispatches the different states +// of the request (ie START, SUCCESS and FAILURE) + +export const pokemonRequest = ({ type, endpoint }) => { + return dispatch => { + dispatch({ type: type.START }); + return fetch(`${POKEAPI_BASE_URL}/${endpoint}`) + .then(res => res.json()) + .then( + data => { + dispatch({ type: type.SUCCESS, payload: data }); + }, + error => { + dispatch({ type: type.FAILURE }); + }, + ) + .catch(eror => { + dispatch({ type: type.FAILURE }); + }); + }; +}; + +// We could have abstracted this function out even more so that it could support +// multiple api's, ajax methods (PUT, POST, etc), and a request body.