Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified public/favicon.ico
Binary file not shown.
11 changes: 8 additions & 3 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<meta name="theme-color" content="#000000" />
<meta
name="description"
Expand All @@ -24,10 +27,12 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>The Road to React</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<noscript
>You need to enable JavaScript to run this app.</noscript
>
<div id="root"></div>
<!--
This HTML file is a template.
Expand Down
245 changes: 42 additions & 203 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,6 @@
import * as React from 'react';
import axios from 'axios';
import { sortBy } from 'lodash';

const API_BASE = 'https://hn.algolia.com/api/v1';
const API_SEARCH = '/search';
const PARAM_SEARCH = 'query=';
const PARAM_PAGE = 'page=';

const getUrl = (searchTerm, page) =>
`${API_BASE}${API_SEARCH}?${PARAM_SEARCH}${searchTerm}&${PARAM_PAGE}${page}`;

const extractSearchTerm = (url) =>
url
.substring(url.lastIndexOf('?') + 1, url.lastIndexOf('&'))
.replace(PARAM_SEARCH, '');

const getLastSearches = (urls) =>
urls
.reduce((result, url, index) => {
const searchTerm = extractSearchTerm(url);

if (index === 0) {
return result.concat(searchTerm);
}

const previousSearchTerm = result[result.length - 1];

if (searchTerm === previousSearchTerm) {
return result;
} else {
return result.concat(searchTerm);
}
}, [])
.slice(-6)
.slice(0, -1);
const API_ENDPOINT = 'https://hn.algolia.com/api/v1/search?query=';

const useSemiPersistentState = (key, initialState) => {
const [value, setValue] = React.useState(
Expand All @@ -60,11 +27,7 @@ const storiesReducer = (state, action) => {
...state,
isLoading: false,
isError: false,
data:
action.payload.page === 0
? action.payload.list
: state.data.concat(action.payload.list),
page: action.payload.page,
data: action.payload,
};
case 'STORIES_FETCH_FAILURE':
return {
Expand All @@ -90,35 +53,28 @@ const App = () => {
'React'
);

const [urls, setUrls] = React.useState([getUrl(searchTerm, 0)]);

const [stories, dispatchStories] = React.useReducer(
storiesReducer,
{ data: [], page: 0, isLoading: false, isError: false }
{ data: [], isLoading: false, isError: false }
);

const handleFetchStories = React.useCallback(async () => {
dispatchStories({ type: 'STORIES_FETCH_INIT' });

try {
const lastUrl = urls[urls.length - 1];
const result = await axios.get(lastUrl);
React.useEffect(() => {
if (!searchTerm) return;

dispatchStories({
type: 'STORIES_FETCH_SUCCESS',
payload: {
list: result.data.hits,
page: result.data.page,
},
});
} catch {
dispatchStories({ type: 'STORIES_FETCH_FAILURE' });
}
}, [urls]);
dispatchStories({ type: 'STORIES_FETCH_INIT' });

React.useEffect(() => {
handleFetchStories();
}, [handleFetchStories]);
fetch(`${API_ENDPOINT}${searchTerm}`)
.then((response) => response.json())
.then((result) => {
dispatchStories({
type: 'STORIES_FETCH_SUCCESS',
payload: result.hits,
});
})
.catch(() =>
dispatchStories({ type: 'STORIES_FETCH_FAILURE' })
);
}, [searchTerm]);

const handleRemoveStory = (item) => {
dispatchStories({
Expand All @@ -127,102 +83,36 @@ const App = () => {
});
};

const handleSearchInput = (event) => {
const handleSearch = (event) => {
setSearchTerm(event.target.value);
};

const handleSearchSubmit = (event) => {
handleSearch(searchTerm, 0);

event.preventDefault();
};

const handleLastSearch = (searchTerm) => {
setSearchTerm(searchTerm);

handleSearch(searchTerm, 0);
};

const handleMore = () => {
const lastUrl = urls[urls.length - 1];
const searchTerm = extractSearchTerm(lastUrl);
handleSearch(searchTerm, stories.page + 1);
};

const handleSearch = (searchTerm, page) => {
const url = getUrl(searchTerm, page);
setUrls(urls.concat(url));
};

const lastSearches = getLastSearches(urls);

return (
<div>
<h1>My Hacker Stories</h1>

<SearchForm
searchTerm={searchTerm}
onSearchInput={handleSearchInput}
onSearchSubmit={handleSearchSubmit}
/>

<LastSearches
lastSearches={lastSearches}
onLastSearch={handleLastSearch}
/>
<InputWithLabel
id="search"
value={searchTerm}
isFocused
onInputChange={handleSearch}
>
<strong>Search:</strong>
</InputWithLabel>

<hr />

{stories.isError && <p>Something went wrong ...</p>}

<List list={stories.data} onRemoveItem={handleRemoveStory} />

{stories.isLoading ? (
<p>Loading ...</p>
) : (
<button type="button" onClick={handleMore}>
More
</button>
<List list={stories.data} onRemoveItem={handleRemoveStory} />
)}
</div>
);
};

const LastSearches = ({ lastSearches, onLastSearch }) => (
<>
{lastSearches.map((searchTerm, index) => (
<button
key={searchTerm + index}
type="button"
onClick={() => onLastSearch(searchTerm)}
>
{searchTerm}
</button>
))}
</>
);

const SearchForm = ({
searchTerm,
onSearchInput,
onSearchSubmit,
}) => (
<form onSubmit={onSearchSubmit}>
<InputWithLabel
id="search"
value={searchTerm}
isFocused
onInputChange={onSearchInput}
>
<strong>Search:</strong>
</InputWithLabel>

<button type="submit" disabled={!searchTerm}>
Submit
</button>
</form>
);

const InputWithLabel = ({
id,
value,
Expand All @@ -244,8 +134,8 @@ const InputWithLabel = ({
<label htmlFor={id}>{children}</label>
&nbsp;
<input
ref={inputRef}
id={id}
ref={inputRef}
type={type}
value={value}
onChange={onInputChange}
Expand All @@ -254,71 +144,20 @@ const InputWithLabel = ({
);
};

const SORTS = {
NONE: (list) => list,
TITLE: (list) => sortBy(list, 'title'),
AUTHOR: (list) => sortBy(list, 'author'),
COMMENT: (list) => sortBy(list, 'num_comments').reverse(),
POINT: (list) => sortBy(list, 'points').reverse(),
};

const List = ({ list, onRemoveItem }) => {
const [sort, setSort] = React.useState({
sortKey: 'NONE',
isReverse: false,
});

const handleSort = (sortKey) => {
const isReverse = sort.sortKey === sortKey && !sort.isReverse;

setSort({ sortKey, isReverse });
};

const sortFunction = SORTS[sort.sortKey];

const sortedList = sort.isReverse
? sortFunction(list).reverse()
: sortFunction(list);

return (
<div>
<div>
<span>
<button type="button" onClick={() => handleSort('TITLE')}>
Title
</button>
</span>
<span>
<button type="button" onClick={() => handleSort('AUTHOR')}>
Author
</button>
</span>
<span>
<button type="button" onClick={() => handleSort('COMMENT')}>
Comments
</button>
</span>
<span>
<button type="button" onClick={() => handleSort('POINT')}>
Points
</button>
</span>
<span>Actions</span>
</div>

{sortedList.map((item) => (
<Item
key={item.objectID}
item={item}
onRemoveItem={onRemoveItem}
/>
))}
</div>
);
};
const List = ({ list, onRemoveItem }) => (
<ul>
{list.map((item) => (
<Item
key={item.objectID}
item={item}
onRemoveItem={onRemoveItem}
/>
))}
</ul>
);

const Item = ({ item, onRemoveItem }) => (
<div>
<li>
<span>
<a href={item.url}>{item.title}</a>
</span>
Expand All @@ -330,7 +169,7 @@ const Item = ({ item, onRemoveItem }) => (
Dismiss
</button>
</span>
</div>
</li>
);

export default App;