|
1 | 1 | import React, { Component } from 'react';
|
2 | 2 | import './App.css';
|
3 | 3 |
|
4 |
| -const list = [ |
5 |
| - { |
6 |
| - title: 'React', |
7 |
| - url: 'https://facebook.github.io/react/', |
8 |
| - author: 'Jordan Walke', |
9 |
| - num_comments: 3, |
10 |
| - points: 4, |
11 |
| - objectID: 0, |
12 |
| - }, |
13 |
| - { |
14 |
| - title: 'Redux', |
15 |
| - url: 'https://github.com/reactjs/redux', |
16 |
| - author: 'Dan Abramov, Andrew Clark', |
17 |
| - num_comments: 2, |
18 |
| - points: 5, |
19 |
| - objectID: 1, |
20 |
| - }, |
21 |
| -]; |
22 |
| - |
23 |
| -const isSearched = searchTerm => item => |
24 |
| - item.title.toLowerCase().includes(searchTerm.toLowerCase()); |
| 4 | +const DEFAULT_QUERY = 'redux'; |
| 5 | +const DEFAULT_HPP = '100'; |
| 6 | + |
| 7 | +const PATH_BASE = 'https://hn.algolia.com/api/v1'; |
| 8 | +const PATH_SEARCH = '/search'; |
| 9 | +const PARAM_SEARCH = 'query='; |
| 10 | +const PARAM_PAGE = 'page='; |
| 11 | +const PARAM_HPP = 'hitsPerPage='; |
25 | 12 |
|
26 | 13 | class App extends Component {
|
27 | 14 | constructor(props) {
|
28 | 15 | super(props);
|
29 | 16 |
|
30 | 17 | this.state = {
|
31 |
| - list, |
32 |
| - searchTerm: '', |
| 18 | + results: null, |
| 19 | + searchKey: '', |
| 20 | + searchTerm: DEFAULT_QUERY, |
| 21 | + error: null, |
33 | 22 | };
|
34 | 23 |
|
| 24 | + this.needsToSearchTopStories = this.needsToSearchTopStories.bind(this); |
| 25 | + this.setSearchTopStories = this.setSearchTopStories.bind(this); |
| 26 | + this.fetchSearchTopStories = this.fetchSearchTopStories.bind(this); |
35 | 27 | this.onSearchChange = this.onSearchChange.bind(this);
|
| 28 | + this.onSearchSubmit = this.onSearchSubmit.bind(this); |
36 | 29 | this.onDismiss = this.onDismiss.bind(this);
|
37 | 30 | }
|
38 | 31 |
|
| 32 | + needsToSearchTopStories(searchTerm) { |
| 33 | + return !this.state.results[searchTerm]; |
| 34 | + } |
| 35 | + |
| 36 | + setSearchTopStories(result) { |
| 37 | + const { hits, page } = result; |
| 38 | + const { searchKey, results } = this.state; |
| 39 | + |
| 40 | + const oldHits = results && results[searchKey] |
| 41 | + ? results[searchKey].hits |
| 42 | + : []; |
| 43 | + |
| 44 | + const updatedHits = [ |
| 45 | + ...oldHits, |
| 46 | + ...hits |
| 47 | + ]; |
| 48 | + |
| 49 | + this.setState({ |
| 50 | + results: { |
| 51 | + ...results, |
| 52 | + [searchKey]: { hits: updatedHits, page } |
| 53 | + } |
| 54 | + }); |
| 55 | + } |
| 56 | + |
| 57 | + fetchSearchTopStories(searchTerm, page = 0) { |
| 58 | + fetch(`${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${searchTerm}&${PARAM_PAGE}${page}&${PARAM_HPP}${DEFAULT_HPP}`) |
| 59 | + .then(response => response.json()) |
| 60 | + .then(result => this.setSearchTopStories(result)) |
| 61 | + .catch(e => this.setState({ error: e })); |
| 62 | + } |
| 63 | + |
| 64 | + componentDidMount() { |
| 65 | + const { searchTerm } = this.state; |
| 66 | + this.setState({ searchKey: searchTerm }); |
| 67 | + this.fetchSearchTopStories(searchTerm); |
| 68 | + } |
| 69 | + |
39 | 70 | onSearchChange(event) {
|
40 | 71 | this.setState({ searchTerm: event.target.value });
|
41 | 72 | }
|
42 | 73 |
|
| 74 | + onSearchSubmit(event) { |
| 75 | + const { searchTerm } = this.state; |
| 76 | + this.setState({ searchKey: searchTerm }); |
| 77 | + |
| 78 | + if (this.needsToSearchTopStories(searchTerm)) { |
| 79 | + this.fetchSearchTopStories(searchTerm); |
| 80 | + } |
| 81 | + |
| 82 | + event.preventDefault(); |
| 83 | + } |
| 84 | + |
43 | 85 | onDismiss(id) {
|
| 86 | + const { searchKey, results } = this.state; |
| 87 | + const { hits, page } = results[searchKey]; |
| 88 | + |
44 | 89 | const isNotId = item => item.objectID !== id;
|
45 |
| - const updatedList = this.state.list.filter(isNotId); |
46 |
| - this.setState({ list: updatedList }); |
| 90 | + const updatedHits = hits.filter(isNotId); |
| 91 | + |
| 92 | + this.setState({ |
| 93 | + results: { |
| 94 | + ...results, |
| 95 | + [searchKey]: { hits: updatedHits, page } |
| 96 | + } |
| 97 | + }); |
47 | 98 | }
|
48 | 99 |
|
49 | 100 | render() {
|
50 |
| - const { searchTerm, list } = this.state; |
| 101 | + const { |
| 102 | + searchTerm, |
| 103 | + results, |
| 104 | + searchKey, |
| 105 | + error, |
| 106 | + } = this.state; |
| 107 | + |
| 108 | + const page = ( |
| 109 | + results && |
| 110 | + results[searchKey] && |
| 111 | + results[searchKey].page |
| 112 | + ) || 0; |
| 113 | + |
| 114 | + const list = ( |
| 115 | + results && |
| 116 | + results[searchKey] && |
| 117 | + results[searchKey].hits |
| 118 | + ) || []; |
| 119 | + |
51 | 120 | return (
|
52 | 121 | <div className="page">
|
53 | 122 | <div className="interactions">
|
54 | 123 | <Search
|
55 | 124 | value={searchTerm}
|
56 | 125 | onChange={this.onSearchChange}
|
| 126 | + onSubmit={this.onSearchSubmit} |
57 | 127 | >
|
58 | 128 | Search
|
59 | 129 | </Search>
|
60 | 130 | </div>
|
61 |
| - <Table |
62 |
| - list={list} |
63 |
| - pattern={searchTerm} |
64 |
| - onDismiss={this.onDismiss} |
65 |
| - /> |
| 131 | + { error |
| 132 | + ? <div className="interactions"> |
| 133 | + <p>Something went wrong.</p> |
| 134 | + </div> |
| 135 | + : <Table |
| 136 | + list={list} |
| 137 | + onDismiss={this.onDismiss} |
| 138 | + /> |
| 139 | + } |
| 140 | + <div className="interactions"> |
| 141 | + <Button onClick={() => this.fetchSearchTopStories(searchKey, page + 1)}> |
| 142 | + More |
| 143 | + </Button> |
| 144 | + </div> |
66 | 145 | </div>
|
67 | 146 | );
|
68 | 147 | }
|
69 | 148 | }
|
70 | 149 |
|
71 |
| -const Search = ({ value, onChange, children }) => |
72 |
| - <form> |
73 |
| - {children} <input |
| 150 | +const Search = ({ |
| 151 | + value, |
| 152 | + onChange, |
| 153 | + onSubmit, |
| 154 | + children |
| 155 | +}) => |
| 156 | + <form onSubmit={onSubmit}> |
| 157 | + <input |
74 | 158 | type="text"
|
75 | 159 | value={value}
|
76 | 160 | onChange={onChange}
|
77 | 161 | />
|
| 162 | + <button type="submit"> |
| 163 | + {children} |
| 164 | + </button> |
78 | 165 | </form>
|
79 | 166 |
|
80 |
| -const Table = ({ list, pattern, onDismiss }) => |
| 167 | +const Table = ({ list, onDismiss }) => |
81 | 168 | <div className="table">
|
82 |
| - {list.filter(isSearched(pattern)).map(item => |
| 169 | + {list.map(item => |
83 | 170 | <div key={item.objectID} className="table-row">
|
84 | 171 | <span style={{ width: '40%' }}>
|
85 | 172 | <a href={item.url}>{item.title}</a>
|
|
0 commit comments