├── public ├── favicon.ico ├── manifest.json └── index.html ├── src ├── App │ ├── App.css │ └── App.js ├── _utils │ ├── Filters.js │ └── Pokemon.js ├── index.css ├── Logo │ ├── Logo.js │ └── Logo.css ├── index.js ├── SearchInput │ ├── search.svg │ ├── close.svg │ ├── SearchInput.js │ └── SearchInput.css ├── SearchResults │ ├── SearchResults.js │ └── SearchResults.css ├── CustomCheckbox │ ├── CustomCheckbox.js │ └── CustomCheckbox.css ├── Pokemon │ ├── pokeball.svg │ ├── Pokemon.js │ └── Pokemon.css ├── Filter │ ├── Filter.js │ └── Filter.css ├── Menu │ ├── Menu.js │ └── Menu.css ├── Search │ └── Search.js └── registerServiceWorker.js ├── package.json ├── .gitignore └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomaszgil/react-tutorial/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/App/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | max-width: 1100px; 3 | padding: 0 30px; 4 | margin: 0 auto; 5 | color: #2a2825; 6 | font-size: 14px; 7 | } 8 | -------------------------------------------------------------------------------- /src/_utils/Filters.js: -------------------------------------------------------------------------------- 1 | const filters = { 2 | SHOW_ALL: 'show all', 3 | ONLY_COLLECTED: 'only collected', 4 | NOT_COLLECTED: 'not collected' 5 | }; 6 | 7 | export default filters; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:400,700'); 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | padding: 0; 10 | font-family: 'Open Sans', sans-serif; 11 | font-size: 100%; 12 | } 13 | -------------------------------------------------------------------------------- /src/Logo/Logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Logo.css'; 3 | 4 | const Logo = () => { 5 | return ( 6 |
7 | 8 | PokeDex 9 |
10 | ); 11 | }; 12 | 13 | export default Logo; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App/App'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /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/SearchInput/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tutorial", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.3.1", 7 | "react-dom": "^16.3.1", 8 | "react-scripts": "1.1.4" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test --env=jsdom", 14 | "eject": "react-scripts eject" 15 | } 16 | } -------------------------------------------------------------------------------- /src/SearchResults/SearchResults.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Pokemon from '../Pokemon/Pokemon'; 3 | import './SearchResults.css'; 4 | 5 | const PokemonContainer = ({ pokemons, isFetched, onPokemonCheck }) => { 6 | const pokemonsComponents = pokemons 7 | .map(pokemon => ); 8 | 9 | if (!isFetched) 10 | return ( 11 |
12 |
13 |
14 | ); 15 | 16 | return ( 17 |
    18 | {pokemonsComponents} 19 |
20 | ); 21 | 22 | }; 23 | 24 | export default PokemonContainer; -------------------------------------------------------------------------------- /src/_utils/Pokemon.js: -------------------------------------------------------------------------------- 1 | export const pokemonTypes = [ 2 | 'bug', 3 | 'dragon', 4 | 'ice', 5 | 'fighting', 6 | 'fire', 7 | 'flying', 8 | 'grass', 9 | 'ghost', 10 | 'ground', 11 | 'electric', 12 | 'normal', 13 | 'poison', 14 | 'psychic', 15 | 'rock', 16 | 'water' 17 | ]; 18 | 19 | export const pokemonTypesToColors = { 20 | 'bug': '#e0e5c3', 21 | 'dragon': '#c8b1db', 22 | 'ice': '#d4f8f9', 23 | 'fighting': '#f2c9c6', 24 | 'fire': '#f9dfb8', 25 | 'flying': '#e2ccfc', 26 | 'grass': '#b8f2b8', 27 | 'ghost': '#bcb4c4', 28 | 'ground': '#d8d1c3', 29 | 'electric': '#ffffa5', 30 | 'normal': '#cecdc4', 31 | 'poison': '#e6b5f2', 32 | 'psychic': '#f7afd8', 33 | 'rock': '#cbccaf', 34 | 'water': '#adcef7' 35 | }; -------------------------------------------------------------------------------- /src/CustomCheckbox/CustomCheckbox.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './CustomCheckbox.css'; 3 | 4 | class CustomCheckbox extends Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.checkedClass = 'checked'; 9 | this.getClassName = this.getClassName.bind(this); 10 | } 11 | 12 | getClassName() { 13 | return this.props.checked ? `custom-checkbox ${this.checkedClass}` : 'custom-checkbox'; 14 | } 15 | 16 | render() { 17 | return ( 18 |
this.props.onClick(this.props.id)}> 19 | 20 | 21 | 22 |
23 | ); 24 | } 25 | } 26 | 27 | export default CustomCheckbox; -------------------------------------------------------------------------------- /src/CustomCheckbox/CustomCheckbox.css: -------------------------------------------------------------------------------- 1 | .custom-checkbox { 2 | padding: 10px; 3 | margin: 5px; 4 | color: #bbb; 5 | } 6 | 7 | .custom-checkbox label { 8 | font-weight: 600; 9 | transition: .5s; 10 | cursor: pointer; 11 | } 12 | 13 | .custom-checkbox input { 14 | display: none; 15 | } 16 | 17 | .custom-checkbox .radio { 18 | width: 10px; 19 | height: 10px; 20 | border-radius: 50%; 21 | border: .15rem solid #bbb; 22 | background-color: #e7e6e4; 23 | display: inline-block; 24 | margin: 0 5px; 25 | cursor: pointer; 26 | transition: all .3s ease-out; 27 | } 28 | 29 | .custom-checkbox.checked .radio { 30 | border-color: #ef52d1; 31 | background-color: #ef52d1; 32 | } 33 | 34 | .custom-checkbox:hover, 35 | .custom-checkbox.checked { 36 | color: #2a2825; 37 | } 38 | 39 | @media only screen and (max-width: 768px) { 40 | .custom-checkbox { 41 | margin: 0 5px; 42 | } 43 | } -------------------------------------------------------------------------------- /src/Pokemon/pokeball.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Svg Vector Icons : http://www.onlinewebfonts.com/icon 6 | 7 | -------------------------------------------------------------------------------- /src/SearchResults/SearchResults.css: -------------------------------------------------------------------------------- 1 | .pokemon-container { 2 | margin: 2rem 0; 3 | padding: 0; 4 | list-style-type: none; 5 | display: grid; 6 | grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); 7 | grid-gap: 1.8rem; 8 | } 9 | 10 | .pokemon-container-loading { 11 | display: flex; 12 | justify-content: center; 13 | } 14 | 15 | .pokemon-container-loading .pokemon { 16 | display: inline-block; 17 | width: 10rem; 18 | height: 10rem; 19 | -webkit-mask: url('../Pokemon/pokeball.svg') no-repeat 100% 100%; 20 | mask: url('../Pokemon/pokeball.svg') no-repeat 100% 100%; 21 | -webkit-mask-size: cover; 22 | mask-size: cover; 23 | background-color: #ddd; 24 | margin: 4rem; 25 | animation: 3s spin infinite cubic-bezier(0.24,-0.35, 0.04, 0.99); 26 | } 27 | 28 | @keyframes spin { 29 | from { 30 | transform: rotate(0deg); 31 | } 32 | 33 | to { 34 | transform: rotate(720deg); 35 | } 36 | } -------------------------------------------------------------------------------- /src/Logo/Logo.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Oxygen:300,400,700'); 2 | 3 | .logo { 4 | font-family: 'Oxygen', sans-serif; 5 | font-size: 1.6rem; 6 | font-weight: 700; 7 | margin: 2rem 0; 8 | display: flex; 9 | justify-content: center; 10 | } 11 | 12 | .logo .icon { 13 | display: inline-block; 14 | width: 2rem; 15 | height: 2rem; 16 | -webkit-mask: url('../Pokemon/pokeball.svg') no-repeat 100% 100%; 17 | mask: url('../Pokemon/pokeball.svg') no-repeat 100% 100%; 18 | -webkit-mask-size: cover; 19 | mask-size: cover; 20 | background-color: #ddd; 21 | margin-right: .8rem; 22 | transition: background-color .3s ease-out; 23 | } 24 | 25 | .logo:hover .icon { 26 | background-color: #ef52d1; 27 | animation: 3s spin infinite cubic-bezier(0.24,-0.35, 0.04, 0.99); 28 | } 29 | 30 | @keyframes spin { 31 | from { 32 | transform: rotate(0deg); 33 | } 34 | 35 | to { 36 | transform: rotate(720deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/SearchInput/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/SearchInput/SearchInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './SearchInput.css'; 3 | 4 | class SearchInput extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { value: '' }; 8 | 9 | this.handleChange = this.handleChange.bind(this); 10 | this.handleClear = this.handleClear.bind(this); 11 | } 12 | 13 | handleChange(e) { 14 | const query = e.target.value; 15 | this.setState({ value: query }); 16 | this.props.onChange(query); 17 | } 18 | 19 | handleClear(e) { 20 | e.preventDefault(); 21 | this.setState({ 22 | value: '' 23 | }); 24 | this.props.onChange(''); 25 | } 26 | 27 | render() { 28 | return ( 29 |
e.preventDefault()}> 30 |
31 | 32 |
33 | 34 |
35 | 36 | ); 37 | } 38 | } 39 | 40 | export default SearchInput; -------------------------------------------------------------------------------- /src/App/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Logo from '../Logo/Logo'; 3 | import Search from '../Search/Search'; 4 | import './App.css'; 5 | 6 | class App extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.apiAccessKey = "RZxUI6ohr3E8hmBGY6HDPlRWpXmVhzgh"; 11 | this.allPokemons = []; 12 | 13 | this.state = { 14 | isFetched: false 15 | }; 16 | 17 | this.fetchPokemons = this.fetchPokemons.bind(this); 18 | } 19 | 20 | componentDidMount() { 21 | this.fetchPokemons(); 22 | } 23 | 24 | fetchPokemons() { 25 | fetch(`https://api.mlab.com/api/1/databases/pokedex/collections/pokemons?apiKey=${this.apiAccessKey}`) 26 | .then(blob => blob.json()) 27 | .then(data => { 28 | this.allPokemons = data.map(element => ({ 29 | name: element.name, 30 | id: element.id, 31 | img: element.image, 32 | type: element.types[0], 33 | collected: false 34 | })); 35 | 36 | this.setState({ 37 | isFetched: true, 38 | }); 39 | }) 40 | .catch(err => console.error(err)); 41 | } 42 | 43 | render() { 44 | return ( 45 |
46 | 47 | 48 |
49 | ); 50 | } 51 | } 52 | 53 | export default App; 54 | -------------------------------------------------------------------------------- /src/Pokemon/Pokemon.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './Pokemon.css' 3 | import { pokemonTypesToColors } from "../_utils/Pokemon"; 4 | 5 | class Pokemon extends Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | collected: this.props.collected 11 | }; 12 | 13 | this.handlePokeballClick = this.handlePokeballClick.bind(this); 14 | } 15 | 16 | handlePokeballClick(e) { 17 | e.preventDefault(); 18 | 19 | this.setState({ 20 | collected: !this.state.collected 21 | }); 22 | 23 | this.props.onPokemonCheck(this.props.id); 24 | } 25 | 26 | render() { 27 | const style = { 28 | background: pokemonTypesToColors[this.props.type] 29 | }; 30 | 31 | return ( 32 |
  • 33 |
  • 46 | ); 47 | } 48 | } 49 | 50 | export default Pokemon; -------------------------------------------------------------------------------- /src/Pokemon/Pokemon.css: -------------------------------------------------------------------------------- 1 | .pokemon .wrapper { 2 | position: relative; 3 | position: sticky; 4 | text-align: center; 5 | cursor: pointer; 6 | } 7 | 8 | .pokemon .img-background { 9 | border-radius: .5rem; 10 | position: absolute; 11 | width: 100%; 12 | height: 100%; 13 | z-index: -1; 14 | transition: transform 0.3s ease-out; 15 | } 16 | 17 | .pokemon img { 18 | padding: 2.5rem; 19 | transition: transform 0.3s ease-out; 20 | } 21 | 22 | .pokemon .wrapper:hover .img-background { 23 | transform: scale(0.9); 24 | } 25 | 26 | .pokemon .wrapper:hover .pokeball { 27 | top: 12%; 28 | right: 10%; 29 | } 30 | 31 | .pokemon .information { 32 | margin-top: .5rem; 33 | font-weight: bold; 34 | display: flex; 35 | flex-wrap: wrap; 36 | } 37 | 38 | .pokemon .name { 39 | margin-right: .5rem; 40 | } 41 | 42 | .pokemon .type { 43 | color: #82817d; 44 | } 45 | 46 | .pokemon .id { 47 | color: #82817d; 48 | margin-left: .5rem; 49 | } 50 | 51 | .pokemon .type:after { 52 | margin-left: .5rem; 53 | content: "\B7"; 54 | } 55 | 56 | .pokemon .pokeball { 57 | display: inline-block; 58 | width: 1rem; 59 | height: 1rem; 60 | -webkit-mask: url('pokeball.svg') no-repeat 100% 100%; 61 | mask: url('pokeball.svg') no-repeat 100% 100%; 62 | -webkit-mask-size: cover; 63 | mask-size: cover; 64 | background-color: #bbb; 65 | margin-right: .4rem; 66 | transform: translateY(.1rem); 67 | transition: all .3s ease-out; 68 | } 69 | 70 | .pokemon.collected .pokeball { 71 | background-color: #ef52d1; 72 | } 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # See https://help.github.com/ignore-files/ for more about ignoring files. 61 | 62 | # dependencies 63 | /node_modules 64 | 65 | # testing 66 | /coverage 67 | 68 | # production 69 | /build 70 | 71 | # misc 72 | .DS_Store 73 | .env.local 74 | .env.development.local 75 | .env.test.local 76 | .env.production.local 77 | 78 | npm-debug.log* 79 | yarn-debug.log* 80 | yarn-error.log* 81 | 82 | # jetbrains 83 | .idea/ 84 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
    29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/SearchInput/SearchInput.css: -------------------------------------------------------------------------------- 1 | .search { 2 | margin: 2rem 0; 3 | } 4 | 5 | .search .search-box { 6 | position: relative; 7 | display: inline-block; 8 | } 9 | 10 | .search input { 11 | font-weight: bold; 12 | font-size: 1rem; 13 | padding: .8rem .8rem .8rem 3rem; 14 | border: .15rem solid #e7e6e4; 15 | border-radius: 3rem; 16 | max-width: 100%; 17 | width: 20rem; 18 | transition: width .3s ease-out .2s; 19 | } 20 | 21 | .search input:focus { 22 | outline: none; 23 | width: 30rem; 24 | } 25 | 26 | .search .icon { 27 | position: absolute; 28 | width: 1.5rem; 29 | height: 1.5rem; 30 | -webkit-mask: url('search.svg') no-repeat 100% 100%; 31 | mask: url('search.svg') no-repeat 100% 100%; 32 | -webkit-mask-size: cover; 33 | mask-size: cover; 34 | background-color: #bbb; 35 | top: 50%; 36 | left: 1.2rem; 37 | transform: translateY(-50%); 38 | transition: background-color .3s ease-out; 39 | } 40 | 41 | .search input:focus ~ .icon { 42 | background-color: #2a2825; 43 | } 44 | 45 | .search .clear { 46 | position: absolute; 47 | width: .8rem; 48 | height: .8rem; 49 | -webkit-mask: url('close.svg') no-repeat 100% 100%; 50 | mask: url('close.svg') no-repeat 100% 100%; 51 | -webkit-mask-size: cover; 52 | mask-size: cover; 53 | background-color: #2a2825; 54 | top: 50%; 55 | right: 1.5rem; 56 | transform: translate(100%, -50%); 57 | opacity: 0; 58 | transition: all .3s ease-out .5s; 59 | } 60 | 61 | 62 | .search input:focus ~ .clear, 63 | .search .clear.visible { 64 | opacity: 1; 65 | transform: translate(0, -50%); 66 | } 67 | 68 | @media only screen and (max-width: 768px) { 69 | .search .search-box { 70 | width: 100%; 71 | } 72 | 73 | .search input, 74 | .search input:focus { 75 | width: 100%; 76 | } 77 | 78 | .search .clear { 79 | transition-delay: 0s; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Filter/Filter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './Filter.css'; 3 | import { pokemonTypes, pokemonTypesToColors } from "../_utils/Pokemon"; 4 | 5 | class Filter extends Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | showMenu: false, 11 | typeFilters: new Set(pokemonTypes) 12 | }; 13 | 14 | this.toggleFilter = this.toggleFilter.bind(this); 15 | this.applyFilter = this.applyFilter.bind(this); 16 | } 17 | 18 | toggleFilter() { 19 | this.setState({ 20 | showMenu: !this.state.showMenu 21 | }); 22 | } 23 | 24 | applyFilter(e) { 25 | const filterElement = e.target; 26 | const filterName = filterElement.dataset.name; 27 | let newFilters = this.state.typeFilters; 28 | 29 | if (this.state.typeFilters.has(filterName)) { 30 | newFilters.delete(filterName); 31 | filterElement.classList.add('disabled'); 32 | } else { 33 | newFilters.add(filterName); 34 | filterElement.classList.remove('disabled'); 35 | } 36 | 37 | this.setState({ 38 | typeFilters: newFilters 39 | }); 40 | this.props.onChange(Array.from(newFilters)); 41 | } 42 | 43 | render() { 44 | return ( 45 |
    46 |
    47 | 48 | Filter Pokedex 49 |
    50 | 51 | {this.state.showMenu && 52 |
    53 |
    54 | Pokemon Type 55 | 56 | { 57 | pokemonTypes.map(type =>
    {type} 62 |
    ) 63 | } 64 |
    65 |
    66 |
    67 | } 68 |
    69 | ); 70 | } 71 | } 72 | 73 | export default Filter; -------------------------------------------------------------------------------- /src/Menu/Menu.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './Menu.css'; 3 | import CustomCheckbox from './../CustomCheckbox/CustomCheckbox'; 4 | import filters from './../_utils/Filters'; 5 | 6 | class Menu extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | filterChecked: filters.SHOW_ALL 12 | }; 13 | 14 | this.filtersList = [ 15 | filters.SHOW_ALL, 16 | filters.ONLY_COLLECTED, 17 | filters.NOT_COLLECTED 18 | ]; 19 | 20 | this.sortingCategory = React.createRef(); 21 | this.sortingDirection = React.createRef(); 22 | this.handleCheckboxClick = this.handleCheckboxClick.bind(this); 23 | this.handleSortingOptionChange = this.handleSortingOptionChange.bind(this); 24 | } 25 | 26 | handleCheckboxClick(filter) { 27 | this.setState({ 28 | filterChecked: filter 29 | }); 30 | 31 | this.props.onFilterChange(filter); 32 | } 33 | 34 | handleSortingOptionChange() { 35 | const category = this.sortingCategory.current.value; 36 | const direction = this.sortingDirection.current.value; 37 | this.props.onSortChange(category, direction); 38 | } 39 | 40 | render() { 41 | return ( 42 |
    43 |
    e.preventDefault()}> 44 | { 45 | this.filtersList.map((label, index) => { 46 | const checked = label === this.state.filterChecked; 47 | return ( 48 | 49 | ); 50 | }) 51 | } 52 | 53 |
    54 |
    Sort by
    55 | 60 | 64 |
    65 |
    66 | ); 67 | } 68 | } 69 | 70 | export default Menu; -------------------------------------------------------------------------------- /src/Menu/Menu.css: -------------------------------------------------------------------------------- 1 | .menu { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: space-between; 5 | border-bottom: .15rem solid #e7e6e4; 6 | padding-bottom: 1rem; 7 | } 8 | 9 | .menu .categories, .menu .sort-by { 10 | display: flex; 11 | flex-direction: row; 12 | cursor: pointer; 13 | } 14 | 15 | .menu .categories { 16 | margin: 0 -15px; 17 | } 18 | 19 | .menu .categories .custom-checkbox label { 20 | font-weight: 600; 21 | transition: .5s; 22 | cursor: pointer; 23 | } 24 | 25 | .menu .categories .custom-checkbox input { 26 | display: none; 27 | } 28 | 29 | .menu .categories .custom-checkbox .radio { 30 | width: 10px; 31 | height: 10px; 32 | border-radius: 50%; 33 | border: .15rem solid #bbb; 34 | background-color: #e7e6e4; 35 | display: inline-block; 36 | margin: 0 5px; 37 | cursor: pointer; 38 | transition: all .3s ease-out; 39 | } 40 | 41 | .menu .categories .custom-checkbox.checked .radio { 42 | border-color: #ef52d1; 43 | background-color: #ef52d1; 44 | } 45 | 46 | .menu .categories .custom-checkbox:hover, 47 | .menu .categories .custom-checkbox.checked { 48 | color: #2a2825; 49 | } 50 | 51 | .menu .sort-by div { 52 | padding: 10px; 53 | margin: 5px; 54 | color: #bbb; 55 | font-weight: 600; 56 | transition: .3s; 57 | } 58 | 59 | select { 60 | background: #fff; 61 | border: 1px solid #eee; 62 | border-radius: 3px; 63 | padding: 3px 22px 3px 3px; 64 | background-image: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M7.406 7.828l4.594 4.594 4.594-4.594 1.406 1.406-6 6-6-6z'%3E%3C/path%3E%3C/svg%3E"); 65 | background-position: calc(100% - 3px) 50%; 66 | background-repeat: no-repeat; 67 | background-size: 16px; 68 | -webkit-appearance: none; 69 | -moz-appearance: none; 70 | } 71 | 72 | select::-ms-expand { 73 | display: none; 74 | } 75 | 76 | .sorting-category { 77 | margin-right: .8rem; 78 | } 79 | 80 | .menu .sort-by select { 81 | border: .15rem solid #e7e6e4; 82 | border-radius: 2rem; 83 | padding: 5px 35px 5px 15px; 84 | color: #121212; 85 | font-weight: 600; 86 | font-size: 1em; 87 | transition: .3s; 88 | } 89 | 90 | .menu .sort-by select:hover { 91 | cursor: pointer; 92 | box-shadow: 0 3px 0 #e7e6e4; 93 | transform: translateY(-3px); 94 | } 95 | 96 | @media only screen and (max-width: 768px) { 97 | .menu .categories { 98 | flex-direction: column; 99 | } 100 | 101 | .menu .categories, .menu .sort-by { 102 | flex-direction: column; 103 | } 104 | 105 | .sorting-category { 106 | margin-right: 0; 107 | margin-bottom: 10px; 108 | } 109 | 110 | .menu .sort-by div { 111 | margin-top: 0; 112 | } 113 | } -------------------------------------------------------------------------------- /src/Filter/Filter.css: -------------------------------------------------------------------------------- 1 | .filter { 2 | 3 | } 4 | 5 | .filter .toggle-filter { 6 | text-align: right; 7 | color: #bbb; 8 | padding: 15px; 9 | transition: .3s; 10 | font-weight: 600; 11 | } 12 | 13 | .filter .toggle-filter:hover { 14 | cursor: pointer; 15 | color: #121212; 16 | } 17 | 18 | .filter .toggle-filter .filter-icon { 19 | transition: .3s; 20 | display: inline-block; 21 | width: 10px; 22 | height: 2px; 23 | margin: 5px; 24 | background-color: #bbb; 25 | } 26 | 27 | .filter .toggle-filter .filter-icon::before { 28 | transition: .3s; 29 | content: ""; 30 | position: absolute; 31 | background-color: #bbb; 32 | display: block; 33 | width: 15px; 34 | height: 2px; 35 | margin: 5px; 36 | transform: translate(-50%, -10px); 37 | } 38 | 39 | .filter .toggle-filter .filter-icon::after { 40 | transition: .3s; 41 | content: ""; 42 | position: absolute; 43 | background-color: #bbb; 44 | display: block; 45 | width: 5px; 46 | height: 2px; 47 | margin: 5px; 48 | transform: translate(-50%, 0); 49 | } 50 | 51 | .filter .toggle-filter:hover .filter-icon, 52 | .filter .toggle-filter:hover .filter-icon::before, 53 | .filter .toggle-filter:hover .filter-icon::after { 54 | background-color: #f95979; 55 | } 56 | 57 | .filter .filter-menu { 58 | display: flex; 59 | transition: height .3s ease-out; 60 | animation: slide-in 1s forwards cubic-bezier(.69,.26,.36,.87); 61 | } 62 | 63 | @keyframes slide-in { 64 | 0% { 65 | transform: translateY(-80px); 66 | margin-bottom: -130px; 67 | opacity: 0; 68 | } 69 | 31% { 70 | opacity: 0; 71 | } 72 | 100% { 73 | transform: translateY(0px); 74 | margin-bottom: 0px; 75 | opacity: 1; 76 | } 77 | } 78 | 79 | .filter .filter-menu.hidden { 80 | height: 0; 81 | } 82 | 83 | .filter .filter-menu { 84 | 85 | } 86 | 87 | .filter .filter-menu div .category-title { 88 | color: #121212; 89 | font-weight: 600; 90 | font-size: 1.2em; 91 | } 92 | 93 | .filter .filter-menu div .category-items { 94 | margin-top: 15px; 95 | display: flex; 96 | flex-wrap: wrap; 97 | justify-content: space-between; 98 | color: #121212; 99 | 100 | } 101 | 102 | .filter .filter-menu div .category-items div { 103 | background-color: #e7e6e4; 104 | flex: 1 0 10%; 105 | color: #121212; 106 | font-weight: 600; 107 | padding: 5px 10px; 108 | margin: 5px; 109 | border-radius: 15px; 110 | transition: .3s; 111 | text-align: center; 112 | } 113 | 114 | .filter .filter-menu div .category-items div:hover { 115 | background-color: #e7e6e4; 116 | color: #121212; 117 | cursor: pointer; 118 | } 119 | 120 | .filter .filter-menu div .category-items .disabled { 121 | background-color: rgba(0,0,0,0) !important; 122 | } 123 | -------------------------------------------------------------------------------- /src/Search/Search.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Menu from '../Menu/Menu'; 3 | import Filter from '../Filter/Filter'; 4 | import SearchInput from '../SearchInput/SearchInput'; 5 | import SearchResults from '../SearchResults/SearchResults'; 6 | import { pokemonTypes } from '../_utils/Pokemon'; 7 | import filters from '../_utils/Filters'; 8 | 9 | class Search extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.allPokemons = this.props.pokemons; 13 | 14 | this.state = { 15 | pokemons: this.allPokemons 16 | }; 17 | 18 | this.criteria = { 19 | searchQuery: '', 20 | sort: { 21 | key: 'id', 22 | direction: 'ascending' 23 | }, 24 | filter: { 25 | types: pokemonTypes, 26 | collected: filters.SHOW_ALL 27 | } 28 | }; 29 | 30 | this.handleSorting = this.handleSorting.bind(this); 31 | this.handleSearchQuery = this.handleSearchQuery.bind(this); 32 | this.handleFilterTypes = this.handleFilterTypes.bind(this); 33 | this.handleFilterCollected = this.handleFilterCollected.bind(this); 34 | this.handlePokemonStateChange = this.handlePokemonStateChange.bind(this); 35 | this.sort = this.sort.bind(this); 36 | this.processSearchQuery = this.processSearchQuery.bind(this); 37 | this.applyFilters = this.applyFilters.bind(this); 38 | this.updateResults = this.updateResults.bind(this); 39 | } 40 | 41 | componentDidMount() { 42 | this.updateResults(); 43 | } 44 | 45 | componentWillReceiveProps(nextProps){ 46 | if (nextProps.isFetched) { 47 | this.allPokemons = nextProps.pokemons; 48 | this.updateResults(); 49 | } 50 | } 51 | 52 | handlePokemonStateChange(id) { 53 | const pokemon = this.allPokemons.find(pokemon => pokemon.id === id); 54 | pokemon.collected = !pokemon.collected; 55 | } 56 | 57 | handleSearchQuery(query) { 58 | this.criteria.searchQuery = query; 59 | this.updateResults(); 60 | } 61 | 62 | handleFilterTypes(types) { 63 | this.criteria.filter.types = types; 64 | this.updateResults(); 65 | } 66 | 67 | handleFilterCollected(filter) { 68 | this.criteria.filter.collected = filter; 69 | this.updateResults(); 70 | } 71 | 72 | handleSorting(key, direction) { 73 | this.criteria.sort.key = key; 74 | this.criteria.sort.direction = direction; 75 | this.updateResults(); 76 | } 77 | 78 | sort(arr) { 79 | let multiplier = 1; 80 | const key = this.criteria.sort.key; 81 | 82 | switch(this.criteria.sort.direction) { 83 | case 'ascending': multiplier = 1; break; 84 | case 'descending': multiplier = -1; break; 85 | default: break; 86 | } 87 | 88 | return arr.sort((a, b) => { 89 | if (a[key] < b[key]) return -1 * multiplier; 90 | if (a[key] === b[key]) return 0; 91 | if (a[key] > b[key]) return multiplier; 92 | 93 | return 0; 94 | }); 95 | } 96 | 97 | processSearchQuery(arr) { 98 | const template = this.criteria.searchQuery.toLowerCase(); 99 | const fields = ['name', 'type', 'id']; 100 | 101 | return arr.filter(pokemon => { 102 | for (let field of fields) 103 | if (pokemon[field].toString().toLowerCase().includes(template)) 104 | return true; 105 | 106 | return false; 107 | }); 108 | } 109 | 110 | applyFilters(arr) { 111 | let result = []; 112 | switch(this.criteria.filter.collected) { 113 | case filters.SHOW_ALL: result = arr; break; 114 | case filters.ONLY_COLLECTED: result = arr.filter(el => el.collected); break; 115 | case filters.NOT_COLLECTED: result = arr.filter(el => !el.collected); break; 116 | default: result = arr; 117 | } 118 | 119 | result = result.filter(pokemon => this.criteria.filter.types.includes(pokemon.type)); 120 | return result; 121 | } 122 | 123 | updateResults() { 124 | let result = this.applyFilters(this.allPokemons); 125 | result = this.processSearchQuery(result); 126 | result = this.sort(result); 127 | 128 | this.setState({ 129 | pokemons: result 130 | }); 131 | } 132 | 133 | render() { 134 | return ( 135 |
    136 | 137 | 138 | 139 | 140 |
    141 | 142 | ); 143 | } 144 | } 145 | 146 | export default Search; -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the App load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web App is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web App. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different App. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Tutorial 2 | 3 | The purpose of this class is to get familiar with React library and build a simple Pokedex - web application displaying pokemons that allows to perform live searching and filtering. You’ll learn how to fetch data from external API, about the idea of building UI with independent components, using state and props to update the view and pass data. 4 | 5 | ### Demo 6 | 7 | You can play around with the application over at [tomaszgil.pl/react-tutorial](http://tomaszgil.pl/react-tutorial). 8 | 9 | ### Get the project’s boilerplate 10 | Clone the repository and switch to branch `step-1`. 11 | ```bash 12 | git clone https://github.com/tomaszgil/react-tutorial.git 13 | git checkout step-1 14 | ``` 15 | ### Starting the project 16 | Once you clone the repository, install modules and run using npm. 17 | ```bash 18 | npm install 19 | npm start 20 | ``` 21 | Alternatively, use yarn. 22 | ```bash 23 | npm install -g yarn 24 | yarn 25 | yarn start 26 | ``` 27 | You should have a development server started at localhost:3000 which has auto reload. Open the code in your preferred editor (we recommend Visual Studio Code or Webstorm). 28 | ### Got lost? Need help? 29 | If you cannot figure out a solution at certain point, you can always go back to see the main version of the code on `master` branch or change the branch to next step (name of the branch corresponds to number of the step e.g. branch `step-4` contains completed steps 1, 2 and 3). 30 | 31 | ### Errors? Important note 32 | The tutorial is designed in a way that your code **might not fully work** after completing some sub-steps. Although, after completing each one from all 6 steps, the application **should not any throw errors**. 33 | 34 | ### Resources 35 | 36 | * [React Documentation](https://reactjs.org/docs/hello-world.html) 37 | * [Virtual DOM and React Diff Algorithm](http://reactkungfu.com/2015/10/the-difference-between-virtual-dom-and-dom/) 38 | * [Official React tutorial](https://reactjs.org/tutorial/tutorial.html) 39 | * [React in 2018](https://tylermcginnis.com/reactjs-tutorial-a-comprehensive-guide-to-building-apps-with-react/) 40 | 41 | 42 | ## 1. Create main App component 43 | First, we need to say, where we want our main App component to render in the html. In `public/index.html` file we can see, that there is an element with an id of root - we want to render our entire application inside that element. Go to `src/index.js`. 44 | 45 | 1.1. Import `App` component from `src/App/App.js` file. Remember that importing in JavaScript always works with relative paths, so pay attention where the file that you're import is located in relation to the file you are importing. React should **throw an error** now saying that nothing is to be imported from this file, once you complete 1.4 this error will go away. 46 | 47 | 1.2. Render `App` inside the element with an id of `root`. 48 | 49 | Go to `src/App/App.js` file. We need to write this component as a class component, since it will utilize React's state. This component will fetch data from external API and pass it down to Search component, which we will create later. 50 | 51 | 1.3. Import React. 52 | 53 | 1.4. Create App class that extends React's Component class. Remember to export the component class after declaring it to be able to import it and use it in other files (use `export default App`). 54 | 55 | 1.5. Component class should have a field to store the data fetched from API. 56 | 57 | 1.6. Initialize the state of the component to have a single variable that indicates whether the data has been already fetched or not. 58 | 59 | 1.7. Create a method that will fetch a pokemon array from this URL: 60 | `https://api.mlab.com/api/1/databases/pokedex/collections/pokemons?apiKey=RZxUI6ohr3E8hmBGY6HDPlRWpXmVhzgh` 61 | 62 | #### Hint 63 | You can use the new Fetch API. This is how you do that: 64 | ```javascript 65 | fetch(dataURL) 66 | .then(blob => blob.json()) 67 | .then(data => { 68 | // data can be processed here 69 | }) 70 | .catch(err => console.error(err)); 71 | ``` 72 | Single pokemon should have id, name, image link, type (take the first one from array with pokemon's types) and boolean information whether it is collected or not (initialize it with false, we will deal with it later). 73 | 74 | 1.8. Invoke this method as soon as the component is mounted onto the page. 75 | 76 | 1.9. We want this component to render a div with a class of app, inside of which we want to have Logo component. You can import that component from `src/Logo/Logo.js` file. 77 | 78 | ## 2. Create Search component with search results 79 | Search component will be responsible for receiving search criteria, narrowing down pokemons array according to these criteria and passing it down to another component, which will render them. First, we will implement displaying pokemons. Go to `src/App/App.js`. 80 | 81 | 2.1. Import Search component and put it after Logo. 82 | 83 | 2.2. Pass the variable from state and the pokemons array as props to Search component. 84 | 85 | Go to `src/Search/Search.js`. 86 | 87 | 2.3. Let’s store pokemons array received through props in class field, as it will later serve as a starting point to any search performed. Also, we want to have pokemons array in state - here we will have results narrowed down by search criteria. 88 | 89 | 2.4. Make sure the class field that stores all pokemons is updated after pokemon data is fetched (which lifecycle method can be used here?). 90 | 91 | 2.5. For now, we need to have this component rendering a single div with SearchResults component inside of it. Let’s pass pokemons array from Search’s state and the information, whether data has been fetched already or not to SearchResults. 92 | 93 | Go to `src/SearchResults/SearchResults.js`. 94 | 95 | 2.6. Let’s write SearchResults component as a function, as it doesn’t need to have its own state. 96 | 97 | 2.7. From each element of pokemons array received from we need to create an actual Pokemon component. 98 | 99 | #### Hint 100 | You can map over pokemon object received through props, returning Pokemon component to which you will pass all pokemon object’s fields as separate props. React will know how to deal with rendering an array of components. Remember to add a unique key property to the component, when you create a list of element. 101 | 102 | 2.8. When data is not fetched, let’s have the component return: 103 | ```html 104 |
    105 |
    106 |
    107 | ``` 108 | 2.9. Otherwise, let’s return a list with a class of `pokemon-container` with created Pokemon components. 109 | ## 3. Create Pokemon component and handle changing collected property 110 | First, we will implement a method that will handle changing pokemon object collected property. Go to `src/Search/Search.js`. 111 | 112 | 3.1. Add a method taking pokemon id as a parameter and toggling the collected field of pokemon with given id. 113 | 114 | 3.2. For now, change the pokemon array in component’s state after the data is fetched. 115 | 116 | 3.3. Pass this function to SearchResults component adding next prop. Pay attention to the context of this function if you are planning to use `this` keyword in there. You might need to perform `this.myMethod = this.myMethod.bind(this)` in the constructor to make sure whenever this function is called, it has got the right context. 117 | 118 | We need to pass that function down to Pokemon itself. Go to `src/SearchResults/SearchResults.js`. 119 | 120 | 3.4. When creating a Pokemon component add a new prop and pass the function received thought props from the parent. 121 | 122 | Now we can focus on Pokemon component itself. Go to `src/Pokemon/Pokemon.js`. 123 | 124 | 3.5. It should have a state with a variable indicating whether it’s collected or not (as we will make some styling changes based upon that). 125 | 126 | 3.6. Add a method to handle click event on the pokemon. It should toggle the collected value from the state and finally invoke the function passed thought props - pass pokemon’s id to it. 127 | 128 | 3.7. Add a render method with html structure shown below: 129 | ```html 130 |
  • 131 |
  • 144 | ``` 145 | 3.8. If pokemon is collected, add an extra class of `collected` to the top li element. 146 | 147 | 3.9. Add a background color to `.img-background` element. You can use color map `pokemonTypesToColors` defined in `src/_utils/Pokemon.js`. 148 | 149 | 3.10. Change static strings to data received in props and attach method that handles clicking on a pokemon to `a.pokeball` element. 150 | ## 4. Implement search input 151 | Go to `src/Search/Search.js`. 152 | 153 | 4.1. Add a new class property that will store our search criteria. For now it can be a JavaScript object with a single property which will store a query string from search input. 154 | 155 | 4.2. Create a method, which will take an array of pokemons as a parameter and return a new array of pokemons, but only those which id, name or type matches the value of string query created in 4.1 (you can use filter function). 156 | 157 | 4.3. Create a method, which will take care of updating the results. For now, it should take array of all pokemons, call the function from 4.2. on it and write the result from it into the pokemon array in the state. 158 | 159 | 4.4. Last method we need to create will be responsible for updating search query in criteria and calling the method updating the results. This function has to be passed to SearchInput component as a prop. Remember to bind this function if necessary. 160 | 161 | 4.5. Add SearchInput component above SearchResults in render function. Remeber to pass aformentioned function to it as a prop. 162 | 163 | Now, let’s go to `src/SearchInput/SearchInput.js`. 164 | 165 | 4.6. We want the render function to generate html of this structure: 166 | ```html 167 |
    168 |
    169 | 170 | 173 | 174 | ``` 175 | 4.7. Implement this component according to “Single Source of Truth” idea. You can read about it here (https://reactjs.org/docs/forms.html#controlled-components). 176 | 177 | 4.8. In the change event handler of the input (`onChange` attribute) remember to call the function passed through props with current input value. 178 | 179 | 4.9. Add a class of `visible` to link with `clear` class when value of the input is not an empty string. Also, add a click handler to that element (`onClick` attribute) which clears the input value and updates the results. 180 | 181 | So far, we covered all basic ideas from React library. We prepared another two components for you to implement, so feel free to proceed further into the tutorial to repeat these ideas. We will give just a brief description of components you are about to implement, they use the concepts you should be already familiar with. 182 | ## 5. Add Menu with filtering and sorting options 183 | 5.1. Add new fields in search criteria, storing the information about sort key, direction and and filter based upon whether a pokemon is collected (you can implement filter constants from `src/_utils/Filters.js`). 184 | 185 | 5.2. Add methods that will handle updating new search criteria and updating search results. 186 | 187 | 5.3. Add methods that will handle filtering and sorting given array according to current search criteria. Update function responsible for updating search results. You might also add initial sorting with default criteria after fetching pokemons. 188 | 189 | 5.4. Implement Menu component to render html structure shown below: 190 | ```html 191 |
    192 |
    193 | { customCheckboxes } 194 |
    195 |
    196 |
    Sort by
    197 | 202 | 206 |
    207 |
    208 | ``` 209 | Make sure `customCheckboxes` is an array of CustomCheckbox components based off of the array of filters from `_utils`, that we already implemented it for you. Look at the implementation, to see which props you need to pass to it in order to have them behave properly. 210 | 211 | 5.5. Implement change event handler for select and click event handler for `CustomCheckbox`. Remember about binding this functions if necessary. 212 | ## 6. Add Filter component 213 | 6.1. To Search component add a new field in search criteria which will store an array of currently chosen pokemon types. You can import types from `src/_utils/Filters.js` file. 214 | 215 | 6.2. Add a method that will take care of updating this field. 216 | 217 | 6.3. Add or modify a function to filter given array upon selected types. 218 | 219 | 6.4. Implement Filter component to render html structure: 220 | ```html 221 |
    222 |
    223 | 224 | Filter Pokedex 225 |
    226 | ``` 227 | Underneath that, show this part only if a user has clicked on the `.toggle-filter` element: 228 | ```html 229 |
    230 |
    231 | Pokemon Type 232 | 233 | { 234 | pokemonTypes.map(type =>
    {type}
    ) 235 | } 236 |
    237 |
    238 |
    239 |
    240 | ``` 241 | 6.5. Add a method that will handle opening and closing the menu with filters. 242 | 243 | 6.6. Add a click event handler to each type pill, which will add or remove corresponding type from the search criteria object and update the results. 244 | --------------------------------------------------------------------------------