From e31fd226c0687535382ec19a9a2686c374e44290 Mon Sep 17 00:00:00 2001 From: sergeyrudenko Date: Thu, 30 May 2019 15:17:43 +0300 Subject: [PATCH] one file app with sort --- package-lock.json | 25 +++- package.json | 6 +- src/App.css | 6 + src/App.js | 187 +++++++++++++++++++++++++---- src/App.test.js | 70 ++++++++++- src/__snapshots__/App.test.js.snap | 181 ++++++++++++++++++++++++++++ 6 files changed, 439 insertions(+), 36 deletions(-) create mode 100644 src/__snapshots__/App.test.js.snap diff --git a/package-lock.json b/package-lock.json index 8e452a4..ab519d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2741,7 +2741,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -3106,7 +3107,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3154,6 +3156,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3192,11 +3195,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -10115,6 +10120,18 @@ "workbox-webpack-plugin": "4.2.0" } }, + "react-test-renderer": { + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.8.6.tgz", + "integrity": "sha512-H2srzU5IWYT6cZXof6AhUcx/wEyJddQ8l7cLM/F7gDXYyPr4oq+vCIxJYXVGhId1J706sqziAjuOEjyNkfgoEw==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "react-is": "^16.8.6", + "scheduler": "^0.13.6" + } + }, "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", diff --git a/package.json b/package.json index fd4d8a0..5311687 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "lodash": "^4.17.11", "react": "^16.8.6", "react-dom": "^16.8.6", "react-scripts": "3.0.1" @@ -28,5 +29,8 @@ "last 1 safari version" ] }, - "proxy": "https://hn.algolia.com/api/v1/search?query=redux" + "proxy": "https://hn.algolia.com/api/v1/search?query=redux", + "devDependencies": { + "react-test-renderer": "^16.8.6" + } } diff --git a/src/App.css b/src/App.css index c588fb5..48a46a3 100644 --- a/src/App.css +++ b/src/App.css @@ -6,6 +6,12 @@ text-align: center; } +.loader { + text-align: center; + display: block; + border: 5px solid #38BB6C; +} + .table { margin: 20px 0; } diff --git a/src/App.js b/src/App.js index 7bff331..619cc25 100644 --- a/src/App.js +++ b/src/App.js @@ -2,6 +2,8 @@ import React, { Component } from 'react'; // import ReactDOM from 'react-dom'; // import logo from './logo.svg'; import './App.css'; +import { sortBy } from 'lodash'; + const DEFAULT_QUERY = 'redux'; const DEFAULT_HPP = '2'; @@ -11,10 +13,23 @@ const PARAM_SEARCH = 'query='; const PARAM_PAGE = 'page='; const PARAM_HPP = 'hitsPerPage='; +const SORTS = { + NONE: list => list, + TITLE: list => sortBy(list, 'title'), + AUTHOR: list => sortBy(list, 'author'), + COMMENTS: list => sortBy(list, 'num_comments').reverse(), + POINTS: list => sortBy(list, 'points').reverse(), +}; // const isSearched = searchTerm => item => // item.title.toLowerCase().includes(searchTerm.toLowerCase()); +const Loading = () => +
Загрузка ...
+const withLoading = (Component) => ({ isLoading, ...rest }) => + isLoading + ? + : class App extends Component { @@ -24,7 +39,10 @@ class App extends Component { results: null, searchKey: '', searchTerm: DEFAULT_QUERY, + isLoading: false, error: null, + sortKey: 'NONE', + isSortReverse: false, }; this.needsToSearchTopStories = this.needsToSearchTopStories.bind(this); @@ -33,12 +51,18 @@ class App extends Component { this.onSearchChange = this.onSearchChange.bind(this); this.onSearchSubmit = this.onSearchSubmit.bind(this); this.fetchSearchTopStories = this.fetchSearchTopStories.bind(this); + this.onSort = this.onSort.bind(this); } needsToSearchTopStories(searchTerm) { return !this.state.results[searchTerm]; } + onSort(sortKey) { + const isSortReverse = this.state.sortKey === sortKey && !this.state.isSortReverse; + this.setState({ sortKey, isSortReverse }); + } + setSearchTopStories(result) { const { hits, page } = result; const { searchKey, results } = this.state; @@ -55,7 +79,8 @@ class App extends Component { results: { ...results, [searchKey]: { hits: updatedHits, page } - } + }, + isLoading: false }); } @@ -86,6 +111,7 @@ class App extends Component { } fetchSearchTopStories(searchTerm, page = 0) { + this.setState({ isLoading: true }); fetch(`${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${searchTerm}&${PARAM_PAGE}${page}&${PARAM_HPP}${DEFAULT_HPP}`) .then(response => response.json()) .then(result => this.setSearchTopStories(result)) @@ -108,7 +134,10 @@ class App extends Component { searchTerm, results, searchKey, - error + error, + isLoading, + sortKey, + isSortReverse, } = this.state; // if (error) { @@ -144,42 +173,117 @@ class App extends Component {

Something went wrong.

: }
- + this.fetchSearchTopStories(searchKey, page + 1)} + isLoading={isLoading} + > + More + +
); } } -const Search = ({ - value, - onChange, - onSubmit, - children -}) => - - - - +class Search extends Component { + componentDidMount() { + if (this.input) { + this.input.focus(); + } + } + render() { + const { + value, + onChange, + onSubmit, + children + } = this.props; + return ( + + { this.input = node; }} + /> + + + ); + } +} -const Table = ({ list = [], onDismiss }) => -
+ + + + +const Table = ({ + list, + sortKey, + isSortReverse, + onSort, + onDismiss +}) => { + const sortedList = SORTS[sortKey](list); + const reverseSortedList = isSortReverse + ? sortedList.reverse() + : sortedList; + + return (
{/* {list.filter(isSearched(pattern)).map(item => */} - {list.map(item => +
+ + + Заголовок + + + + + Автор + + + + + Комментарии + + + + + Очки + + + + Архив + +
+ {reverseSortedList.map(item =>
{item.title} @@ -204,8 +308,8 @@ const Table = ({ list = [], onDismiss }) =>
)} -
- +
); +} const Button = ({ onClick, className = '', children }) => +const Sort = ({ + sortKey, + activeSortKey, + onSort, + children +}) => { + const sortClass = ['button-inline']; + if (sortKey === activeSortKey) { + sortClass.push('button-active'); + } + return ( + + ); +} + + + +const ButtonWithLoading = withLoading(Button); + export default App; + +export { + Button, + Search, + Table, +}; + diff --git a/src/App.test.js b/src/App.test.js index a754b20..7bef9b5 100644 --- a/src/App.test.js +++ b/src/App.test.js @@ -1,9 +1,69 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import App from './App'; +import renderer from 'react-test-renderer'; +import App, { Search, Button, Table } from './App'; -it('renders without crashing', () => { - const div = document.createElement('div'); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); +describe('App', () => { + it('отрисовывает без ошибки', () => { + const div = document.createElement('div'); + ReactDOM.render(, div); + ReactDOM.unmountComponentAtNode(div); + }); + test('есть корректный снимок', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); + +describe('Search', () => { + it('отрисовывает без ошибки', () => { + const div = document.createElement('div'); + ReactDOM.render(Поиск, div); + ReactDOM.unmountComponentAtNode(div); + }); + test('есть корректный снимок', () => { + const component = renderer.create( + Поиск + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); + +describe('Button', () => { + it('отрисовывает без ошибки', () => { + const div = document.createElement('div'); + ReactDOM.render(, div); + ReactDOM.unmountComponentAtNode(div); + }); + test('есть корректный снимок', () => { + const component = renderer.create( + + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); + +describe('Table', () => { + const props = { + list: [ + { title: '1', author: '1', num_comments: 1, points: 2, objectID: 'y' }, + { title: '2', author: '2', num_comments: 1, points: 2, objectID: 'z' }, + ], + }; + it('отрисовывает без ошибки', () => { + const div = document.createElement('div'); + ReactDOM.render(
, div); + }); + test('есть корректный снимок', () => { + const component = renderer.create( +
+ ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); }); diff --git a/src/__snapshots__/App.test.js.snap b/src/__snapshots__/App.test.js.snap new file mode 100644 index 0000000..6a35085 --- /dev/null +++ b/src/__snapshots__/App.test.js.snap @@ -0,0 +1,181 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`App есть корректный снимок 1`] = ` +
+
+
+ + + +
+
+
+
+ Загрузка ... +
+
+
+`; + +exports[`Button есть корректный снимок 1`] = ` + +`; + +exports[`Search есть корректный снимок 1`] = ` +
+ + + +`; + +exports[`Table есть корректный снимок 1`] = ` +
+
+ + + 1 + + + + 1 + + + 1 + + + 2 + + + + +
+
+ + + 2 + + + + 2 + + + 1 + + + 2 + + + + +
+
+`;